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

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的(未删减版)的函数都遵循这样的模式:

获取一个JWT用于通过鉴权

创建一个用于和UDR通信的client

向UDR发送请求做相应的数据曾删改查

处理从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)

}

相关推荐

合作伙伴