Free5GC源码研究(6) - UDM&UDR研究

本文研究 Unified Data Management (UDM) 和 Unified Data Repository(UDR)主要实现的功能

UDM是5G网络中统一数据管理的NF,主要管理的是用户的订阅数据和设备的状态数据。UDR则是5G中统一的数据仓库,主要负责存储各种各样的数据,包括但不限于UDM管理的数据。

之所以把UDM和UDR合并在一起研究,原因有两个。一是他们之间实在联系紧密:再free5gc中UDM中的几乎所有处理函数都是做些数据处理然后调用UDR的接口对数据增删改查。二是UDM和UDR的逻辑都很简单:简单数据处理再调用外部接口,所以UDM中的很多代码都遵循大同小异的模式,而UDR中的处理函数甚至都是自动生成的。这两个NF看起来代码量很多,这只是因为他们要处理的数据类型多而已,对每个数据类型的操作都很简单。

我们可以看看一个例子。下面是UDM管理"Amf3gppAccess"这类数据的简化版代码

// https://github.com/free5gc/udm/blob/v1.2.3/internal/sbi/processor/ue_context_management.go
func (p *Processor) GetAmf3gppAccessProcedure(c *gin.Context, ueID string, supportedFeatures string) {
	var queryAmfContext3gppParamOpts Nudr_DataRepository.QueryAmfContext3gppParamOpts
	queryAmfContext3gppParamOpts.SupportedFeatures = optional.NewString(supportedFeatures)
	clientAPI, err := p.Consumer().CreateUDMClientToUDR(ueID)
	amf3GppAccessRegistration, resp, err := clientAPI.AMF3GPPAccessRegistrationDocumentApi.
		QueryAmfContext3gpp(ctx, ueID, &queryAmfContext3gppParamOpts)
	c.JSON(http.StatusOK, amf3GppAccessRegistration)
}


func (p *Processor) RegisterAmfNon3gppAccessProcedure(c *gin.Context,
	registerRequest models.AmfNon3GppAccessRegistration,
	ueID string,
) {
	p.Context().CreateAmfNon3gppRegContext(ueID, registerRequest)
	clientAPI, err := p.Consumer().CreateUDMClientToUDR(ueID)
	var createAmfContextNon3gppParamOpts Nudr_DataRepository.CreateAmfContextNon3gppParamOpts
	createAmfContextNon3gppParamOpts.AmfNon3GppAccessRegistration = optional.NewInterface(registerRequest)
	resp, err := clientAPI.AMFNon3GPPAccessRegistrationDocumentApi.CreateAmfContextNon3gpp(
		ctx, ueID, &createAmfContextNon3gppParamOpts)

	// TS 23.502 4.2.2.2.2 14d: UDM initiate a Nudm_UECM_DeregistrationNotification to the old AMF
	// corresponding to the same (e.g. 3GPP) access, if one exists
    var oldAmfNon3GppAccessRegContext *models.AmfNon3GppAccessRegistration
	if p.Context().UdmAmfNon3gppRegContextExists(ueID) {
		ue, _ := p.Context().UdmUeFindBySupi(ueID)
		oldAmfNon3GppAccessRegContext = ue.AmfNon3GppAccessRegistration
	}
	if oldAmfNon3GppAccessRegContext != nil {
		deregistData := models.DeregistrationData{
			DeregReason: models.DeregistrationReason_UE_INITIAL_REGISTRATION,
			AccessType:  models.AccessType_NON_3_GPP_ACCESS,
		}
		p.SendOnDeregistrationNotification(ueID, oldAmfNon3GppAccessRegContext.DeregCallbackUri,
			deregistData) // Deregistration Notify Triggered
	} else {
		udmUe, _ := p.Context().UdmUeFindBySupi(ueID)
		c.Header("Location", udmUe.GetLocationURI(udm_context.LocationUriAmfNon3GppAccessRegistration))
		c.JSON(http.StatusCreated, registerRequest)
	}
}

func (p *Processor) UpdateAmf3gppAccessProcedure(c *gin.Context,
	request models.Amf3GppAccessRegistrationModification,
	ueID string,
) {
	var patchItemReqArray []models.PatchItem
	currentContext := p.Context().GetAmf3gppRegContext(ueID)

    // check Guami/PurgeFlag/Pei/ImsVoPs/BackupAMfInfo of request
    // new patchItemTmp of models.PatchItem, set its fields
    // patchItemReqArray = append(patchItemReqArray, patchItemTmp)

	clientAPI, err := p.Consumer().CreateUDMClientToUDR(ueID)
	resp, err := clientAPI.AMF3GPPAccessRegistrationDocumentApi.AmfContext3gpp(ctx, ueID,
		patchItemReqArray)
	c.Status(http.StatusNoContent)
}

概括来说,大部分UDM的(未删减版)的函数都遵循这样的模式:

  1. 获取一个JWT用于通过鉴权
  2. 创建一个用于和UDR通信的client
  3. 向UDR发送请求做相应的数据曾删改查
  4. 处理从UDR来的回应,包括各种error

少数函数还会做点额外的操作,比如上面代码中的RegisterAmfNon3gppAccessProcedure在创建一个"Amf3gppAccess"后,还会检查一下有没有旧的"Amf3gppAccess",有的话就要删掉。

而下面则是UDR中和"Amf3gppAccess"相关的接口(无删减)。可以看到,这些代码完全由OpenAPI Generator自动生成,而且这些函数做的都是根据喊出参参数构建一个filter,然后调用mongoapi的接口执行增删改查操作

/* https://github.com/free5gc/udr/blob/main/internal/sbi/processor/amf3_gpp_access_registration_document.go */
/*
 * Nudr_DataRepository API OpenAPI file
 *
 * Unified Data Repository Service
 *
 * API version: 1.0.0
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package processor

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/bson"

	"github.com/free5gc/openapi/models"
	"github.com/free5gc/udr/internal/logger"
	"github.com/free5gc/udr/internal/util"
	"github.com/free5gc/util/mongoapi"
)

func (p *Processor) AmfContext3gppProcedure(
	c *gin.Context, collName string, ueId string, patchItem []models.PatchItem,
) {
	var origValue, newValue map[string]interface{}
	var err error
	filter := bson.M{"ueId": ueId}
	if origValue, newValue, err = p.PatchDataToDBAndNotify(collName, ueId, patchItem, filter); err != nil {
		logger.DataRepoLog.Errorf("AmfContext3gppProcedure err: %+v", err)
		problemDetails := util.ProblemDetailsModifyNotAllowed("")
		c.JSON(int(problemDetails.Status), problemDetails)
	}

	PreHandleOnDataChangeNotify(ueId, CurrentResourceUri, patchItem, origValue, newValue)
	c.Status(http.StatusNoContent)
}

func (p *Processor) CreateAmfContext3gppProcedure(c *gin.Context, collName string, ueId string,
	Amf3GppAccessRegistration models.Amf3GppAccessRegistration,
) {
	filter := bson.M{"ueId": ueId}
	putData := util.ToBsonM(Amf3GppAccessRegistration)
	putData["ueId"] = ueId

	if _, err := mongoapi.RestfulAPIPutOne(collName, filter, putData); err != nil {
		logger.DataRepoLog.Errorf("CreateAmfContext3gppProcedure err: %+v", err)
	}
	c.Status(http.StatusNoContent)
}

func (p *Processor) QueryAmfContext3gppProcedure(c *gin.Context, collName string, ueId string) {
	filter := bson.M{"ueId": ueId}
	data, pd := p.GetDataFromDB(collName, filter)
	if pd != nil {
		logger.DataRepoLog.Errorf("QueryAmfContext3gppProcedure err: %s", pd.Detail)
		c.JSON(int(pd.Status), pd)
	}
	c.JSON(http.StatusOK, data)
}

和UDM类似,几乎所有的UDR代码都这样的操作,还都是由OpenAPI Generator自动生成的。那么UDM处理的数据类型和UDR存储的数据类型有哪些呢?UDM管理的数据主要由以下几类

  • Event Exposure Subscription 当NF想要某个用户设备发生了什么事件(比如断联了)时被通知到,那就向UDM订阅关于这个设备的事件。当然,不再感兴趣时就可以取消订阅。
  • Provisioned Parameter 这类数据时为用户设备预设的参数,用于定义这个设备的各种行为以及与网络的交互方式,包括但不限于通话质量、漫游设置、安全设置等等。
  • Subscriber Data 这里的subscriber指的是我们日常语境里的订阅者,也就是“与移动网络运营商,比如中国移动和中国联通,签订服务合同的个人或实体。”这些订阅者数据包括我们能够使用的服务类型(语音通话、SMS短信、移动网络流量等),服务质量,特殊限制等。
  • UE Context 这里指的是当前设备的状态上下文,比如AMF和SMF的注册信息。
  • Notification 这里的notification指的是更细粒度、更重要两类事件。第一类是用户数据的变更,订阅者希望某个用户的某项数据发生变更时被通知到。第二类是用户设备从网络中注销,订阅者希望知道注销的原因
  • Authentication 也就是用户设备的鉴权状态,并负责计算生成鉴权向量

以上是UDM主要管理的数据。而UDR存储的数据远不止于UDM的管理数据。通常情况下,我们会期望所有的数据操作都通过UDR进行。使用UDR来存储数据可以确保数据的中心化、一致性、可规模化、以及方便其他所有NF使用。然而,前文我们也看过NF_Management把关于NF的数据保存在了本地的数据库中。为什么NRF要直接调用数据库接口而不是通过UDR来操作呢?首先我们要明确一点,5G标准没有规定NF的数据必须存储在UDR中或者NRF自己的数据库中,因此可以根据软件团队的自己的考虑来实现。从架构的一致性和可维护性角度来看,通过 UDR 来管理所有数据访问通常是更好的做法。在free5gc中,NRF的数据保存在自己的本地数据库里,可能的原因是free5gc的开发团队认为NRF的数据并不会被其他NF使用,也不会有很大的规模,所以没有必要保存在UDR里。既然如此,让NRF把数据保存在本地不仅可以减少延迟提高效率、还可以为NRF的数据专门开发一些功能,比如各种查询功能,而不必考虑会干扰到其他数据(Separation of Concerns)。


前文说到,UDM几乎所有代码都是在针对某一类数据稍做处理后调用UDR的接口。而generate_auth_data.go就是一个例外。它做的事情是支持AUSF的鉴权机制。generate_auth_data.go中有两个函数,其中ConfirmAuthDataProcedure主要目的是在UDR中记录用户设备鉴权的结果,这可能会用于跟踪用户的认证历史、检测潜在的安全问题,以及满足某些监管要求。它是整个认证流程中的一个步骤,通常在成功完成认证后调用。

// https://github.com/free5gc/udm/blob/v1.2.3/internal/sbi/processor/generate_auth_data.go#L79
func (p *Processor) ConfirmAuthDataProcedure(c *gin.Context,
	authEvent models.AuthEvent,
	supi string,
) {
	var createAuthParam Nudr_DataRepository.CreateAuthenticationStatusParamOpts
	createAuthParam.AuthEvent = optional.NewInterface(authEvent)

	client, err := p.Consumer().CreateUDMClientToUDR(supi)
	resp, err := client.AuthenticationStatusDocumentApi.CreateAuthenticationStatus(
		ctx, supi, &createAuthParam)
	c.Status(http.StatusCreated)
}

第二个函数,也是最重要的函数GenerateAuthDataProcedure。这回做的不是调用UDR的接口做增删改查,而是是应AUSF的请求计算相应的鉴权向量(Anthentication Vector)。整个函数极其复杂,哪怕经过简化去掉各种打日志的逻辑和错误处理的逻辑,剩下的代码量也相当大。不过总的来说,这个函数做的事情就是从UDR中查询用户对应的AuthSub(这是用户和运营商签约时运营商为用户生成并保存到网络UDR中的),并从中提取一系列数据,然后运行milenage算法生成鉴权向量,最后把它返回给AUSF。

点击查看GenerateAuthDataProcedure的简化版代码
// https://github.com/free5gc/udm/blob/v1.2.3/internal/sbi/processor/generate_auth_data.go#L121
func (p *Processor) GenerateAuthDataProcedure(
	c *gin.Context,
	authInfoRequest models.AuthenticationInfoRequest,
	supiOrSuci string,
) {
	response := &models.AuthenticationInfoResult{}
	supi, err := suci.ToSupi(supiOrSuci, p.Context().SuciProfiles)
	client, err := p.Consumer().CreateUDMClientToUDR(supi)
	authSubs, res, err := client.AuthenticationDataDocumentApi.QueryAuthSubsData(ctx, supi, nil)

	/*
		K, RAND, CK, IK: 128 bits (16 bytes) (hex len = 32)
		SQN, AK: 48 bits (6 bytes) (hex len = 12) TS33.102 - 6.3.2
		AMF: 16 bits (2 bytes) (hex len = 4) TS33.102 - Annex H
	*/

	var kStr, opStr, opcStr string
	var k, op, opc []byte

	kStr = authSubs.PermanentKey.PermanentKeyValue
	k, err = hex.DecodeString(kStr)
	opStr = authSubs.Milenage.Op.OpValue
	op, err = hex.DecodeString(opStr)
	opcStr = authSubs.Opc.OpcValue
	opc, err = hex.DecodeString(opcStr)

	sqnStr := p.strictHex(authSubs.SequenceNumber, 12)
	sqn, err := hex.DecodeString(sqnStr)
	RAND := make([]byte, 16)
	cryptoRand.Read(RAND)
	amfStr := p.strictHex(authSubs.AuthenticationManagementField, 4)
	AMF, err := hex.DecodeString(amfStr)

	// increment sqn
	bigSQN := big.NewInt(0)
	sqn, err = hex.DecodeString(sqnStr)

	bigSQN.SetString(sqnStr, 16)

	bigInc := big.NewInt(1)
	bigSQN = bigInc.Add(bigSQN, bigInc)

	SQNheStr := fmt.Sprintf("%x", bigSQN)
	SQNheStr = p.strictHex(SQNheStr, 12)
	patchItemArray := []models.PatchItem{
		{
			Op:    models.PatchOperation_REPLACE,
			Path:  "/sequenceNumber",
			Value: SQNheStr,
		},
	}

	var rsp *http.Response
	rsp, err = client.AuthenticationDataDocumentApi.ModifyAuthentication(
		ctx, supi, patchItemArray)
	
	// Run milenage
	macA, macS := make([]byte, 8), make([]byte, 8)
	CK, IK := make([]byte, 16), make([]byte, 16)
	RES := make([]byte, 8)
	AK, AKstar := make([]byte, 6), make([]byte, 6)

	// Generate macA, macS
	milenage.F1(opc, k, RAND, sqn, AMF, macA, macS)
	// Generate RES, CK, IK, AK, AKstar
	// RES == XRES (expected RES) for server
	milenage.F2345(opc, k, RAND, RES, CK, IK, AK, AKstar)

	// Generate AUTN
	SQNxorAK := make([]byte, 6)
	for i := 0; i < len(sqn); i++ {
		SQNxorAK[i] = sqn[i] ^ AK[i]
	}
	AUTN := append(append(SQNxorAK, AMF...), macA...)

	var av models.AuthenticationVector
	if authSubs.AuthenticationMethod == models.AuthMethod__5_G_AKA {
		response.AuthType = models.AuthType__5_G_AKA

		// derive XRES*
		key := append(CK, IK...)
		FC := ueauth.FC_FOR_RES_STAR_XRES_STAR_DERIVATION
		P0 := []byte(authInfoRequest.ServingNetworkName)
		P1 := RAND
		P2 := RES

		kdfValForXresStar, err := ueauth.GetKDFValue(
			key, FC, P0, ueauth.KDFLen(P0), P1, ueauth.KDFLen(P1), P2, ueauth.KDFLen(P2))
		if err != nil {
			logger.UeauLog.Errorf("Get kdfValForXresStar err: %+v", err)
		}
		xresStar := kdfValForXresStar[len(kdfValForXresStar)/2:]
		logger.UeauLog.Tracef("xresStar=[%x]", xresStar)

		// derive Kausf
		FC = ueauth.FC_FOR_KAUSF_DERIVATION
		P0 = []byte(authInfoRequest.ServingNetworkName)
		P1 = SQNxorAK
		kdfValForKausf, err := ueauth.GetKDFValue(key, FC, P0, ueauth.KDFLen(P0), P1, ueauth.KDFLen(P1))

		// Fill in rand, xresStar, autn, kausf
		av.Rand = hex.EncodeToString(RAND)
		av.XresStar = hex.EncodeToString(xresStar)
		av.Autn = hex.EncodeToString(AUTN)
		av.Kausf = hex.EncodeToString(kdfValForKausf)
		av.AvType = models.AvType__5_G_HE_AKA
	} else { // EAP-AKA'
		response.AuthType = models.AuthType_EAP_AKA_PRIME

		// derive CK' and IK'
		key := append(CK, IK...)
		FC := ueauth.FC_FOR_CK_PRIME_IK_PRIME_DERIVATION
		P0 := []byte(authInfoRequest.ServingNetworkName)
		P1 := SQNxorAK
		kdfVal, err := ueauth.GetKDFValue(key, FC, P0, ueauth.KDFLen(P0), P1, ueauth.KDFLen(P1))
		// For TS 35.208 test set 19 & RFC 5448 test vector 1
		// CK': 0093 962d 0dd8 4aa5 684b 045c 9edf fa04
		// IK': ccfc 230c a74f cc96 c0a5 d611 64f5 a76

		ckPrime := kdfVal[:len(kdfVal)/2]
		ikPrime := kdfVal[len(kdfVal)/2:]
		logger.UeauLog.Tracef("ckPrime=[%x], kPrime=[%x]", ckPrime, ikPrime)

		// Fill in rand, xres, autn, ckPrime, ikPrime
		av.Rand = hex.EncodeToString(RAND)
		av.Xres = hex.EncodeToString(RES)
		av.Autn = hex.EncodeToString(AUTN)
		av.CkPrime = hex.EncodeToString(ckPrime)
		av.IkPrime = hex.EncodeToString(ikPrime)
		av.AvType = models.AvType_EAP_AKA_PRIME
	}

	response.AuthenticationVector = &av
	response.Supi = supi
	c.JSON(http.StatusOK, response)
}
posted @ 2024-10-20 22:23  zrq96  阅读(34)  评论(0编辑  收藏  举报