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) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY