本文研究 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)
}