Free5GC源码研究(5) - NRF研究

本文研究 Network Repository Function (NRF) 主要实现的功能

NRF的概念

NRF在5G网络架构中有着中心意义——所有NF在刚上线时就要向网络中的NRF报告自己的存在,告知NRF自己的基本信息,同时在即将下线时也要像NRF报告自己的不存在;而某个NF想要调用其他NF的功能时,需要向NRF询问网络中存在哪些能满足需求的NF,拿到这些NF的API接口然后才能访问它们的服务。所以,NRF就是5G网络里的中介,谁有需求的话就去问NRF,NRF会给它介绍能为它解决问题的服务。当然,这就要求所有的NF在最初就知道NRF的API端口,这一般需要人工配置。比如,AUSF的配置文件就有一项nrfUri

具体而言,5G标准TS29.510定义了NRF的应该提供的API服务

img

在着几个服务里,Nnrf_NFManagement负责网络中NF信息的管理,实际上就是数据的增删改查;Nnrf_NFDiscovery则为NF提供“咨询服务”,给NF介绍能为其解决问题的其他NF;Nnrf_AccessToken则是为NF提供鉴权机制(AUSF是鉴别某个用户是否具备权限访问网络,NRF是鉴别某个NF是否具备权限访问其他NF);Nnrf_Bootstrapping则是一个更高层的服务,用来告知其他NF本NRF能提供什么服务,以及对应的API接口是什么(然而现阶段的free5gc/nrf并没有实现这个服务)。

NRF的实现

Context

考察NF的源代码,最重要的NFContext数据类型自然不能忽略,下面是NRF的Context定义:

https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/context.go#L21
type NRFContext struct {
	NrfNfProfile     models.NfProfile
	Nrf_NfInstanceID string
	RootPrivKey      *rsa.PrivateKey
	RootCert         *x509.Certificate
	NrfPrivKey       *rsa.PrivateKey
	NrfPubKey        *rsa.PublicKey
	NrfCert          *x509.Certificate
	NfRegistNum      int
	nfRegistNumLock  sync.RWMutex
}

可见这个NRFContext只是保存了一些很基本的信息,比如其ID和Profile,NfRegistNum用来跟踪网络中有多少个活跃的NF,其他都是各种加密解密用的key。前文我们说到NRF有管理网络中各个NF基本信息的功能,然而它的NFContext中却看不到存储大批量数据的容器结构。相比之下,AUSFCOontext可是用了两个Map类型来存储用户设备的数据。NRF不在Context中存储NF的数据,那就肯定保存在其他地方了。也许是考虑到了持久化和数据一致性问题,尤其是NRF需要提供查询服务,所以要free5gc选择把NRF的数据保存到数据库中,这样一来NRF可以利用数据库的查询引擎来提供查询服务了。

具体来说,NRF选择使用MongoDB,保存在名为free5gc的数据库的NfProfile的集合中:

# https://github.com/free5gc/free5gc/blob/main/config/nrfcfg.yaml#L5
configuration:
  MongoDBName: free5gc # database name in MongoDB
  MongoDBUrl: mongodb://127.0.0.1:27017 # a valid URL of the mongodb
// https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/context.go#L33
const (
	NfProfileCollName string = "NfProfile"
)

NF_Management以及MongoDB

对网络中各NF的信息的管理是NRF所有功能的基础,而这所谓的数据管理就是对数据库里的信息做增删改查。free5gc选择的数据库是MongoDB,还把对其的各种操作封装在了free5gc/util里。下面是NRF根据某个NF实例的ID在数据库做查找的简化版代码:

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_management.go#L370
import (
    "github.com/free5gc/util/mongoapi"
    "github.com/gin-gonic/gin"
)
func (p *Processor) GetNFInstanceProcedure(c *gin.Context, nfInstanceID string) {
	collName := nrf_context.NfProfileCollName
	filter := bson.M{"nfInstanceId": nfInstanceID}
	response, err := mongoapi.RestfulAPIGetOne(collName, filter)
	c.JSON(http.StatusOK, response)
}

着急哈简化版代码显示了NF_Management里各种服务的核心逻辑:获取MongoDB中对应的Collection Name(相当于SQL数据库里的Table),构建一个filter(相当于SQL数据库里的query),然后调用封装好的函数对数据库进行操作(在上面的函数里做的事查找),最后返回相应的结果。除了查找之外,当然还可以对数据做新增、修改、删除等,这些操作对应的mongoapi函数如下

// https://github.com/free5gc/util/blob/v1.0.6/mongoapi/mongoapi.go
func RestfulAPIGetMany(collName string, filter bson.M, argOpt ...interface{}) ([]map[string]interface{}, error)
func RestfulAPIGetOne(collName string, filter bson.M, argOpt ...interface{}) (result map[string]interface{}, err error)
func RestfulAPIDeleteMany(collName string, filter bson.M, argOpt ...interface{}) error
func RestfulAPIDeleteOne(collName string, filter bson.M, argOpt ...interface{}) error
func RestfulAPIPutOne(collName string, filter bson.M, putData map[string]interface{}, ...) (bool, error)
func RestfulAPIPutOneNotUpdate(collName string, filter bson.M, putData map[string]interface{}, ...) (bool, error)
func RestfulAPIPost(collName string, filter bson.M, postData map[string]interface{}, argOpt ...interface{}) (bool, error)
func RestfulAPIPostMany(collName string, filter bson.M, postDataArray []interface{}) error 
func RestfulAPIJSONPatch(collName string, filter bson.M, patchJSON []byte, argOpt ...interface{}) error
// ...

增删改的逻辑比查的逻辑相对来说复杂一点,这里以NF的新增为例:

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_management.go#L390
func (p *Processor) NFRegisterProcedure(c *gin.Context, nfProfile models.NfProfile) {
	var nf models.NfProfile
	nrf_context.NnrfNFManagementDataModel(&nf, nfProfile)

	// Marshal nf to bson
	tmp, err := json.Marshal(nf)
	putData := bson.M{}
	json.Unmarshal(tmp, &putData)
	
	// set db info
	collName := nrf_context.NfProfileCollName
	nfInstanceId := nf.NfInstanceId
	filter := bson.M{"nfInstanceId": nfInstanceId}

	// Update NF Profile case
	mongoapi.RestfulAPIPutOne(collName, filter, putData)

    // notify related NFs
    uriList := nrf_context.GetNofificationUri(nf)
    Notification_event := models.NotificationEventType_REGISTERED
    for _, uri := range uriList {
        p.Consumer().SendNFStatusNotify(Notification_event, nfInstanceUri, uri, &nfProfile)
    }
    c.JSON(http.StatusCreated, putData)
}

NF的修改UpdateNFInstanceProcedure(c *gin.Context, nfProfile models.NfProfile)和删除NFDeregisterProcedure(nfInstanceID string)也都是相似的逻辑,只不过对应的mongoapi操作不一样而已。在上面的代码我们可以看到在对数据库操作以后,还要通知相关NF,最后才是返回。这些“相关的NF”是哪里来的?这就是NF_Management要管理的另一类数据subscription(注意是NF的subscription,不是用户设备的subscription):如果某个nf_a很关心另一类NF的动向(比如AUSF的实例都很关心UDM的动向,因为他们的鉴权服务依赖UDM的数据服务),那么它可以向NRF订阅关于这类NF的消息,每当有变动(被增、删、改),NRF就会告知nf_a这些变动。那么对应的,就会有关于subscription的增删改查,比如下面的简化版代码,就把一个subscription存到了数据库中

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_management.go#L127
func (p *Processor) CreateSubscriptionProcedure(
	subscription models.NrfSubscriptionData,
) (bson.M, *models.ProblemDetails) {
	subscriptionID, err := nrf_context.SetsubscriptionId()
	subscription.SubscriptionId = subscriptionID

	tmp, err := json.Marshal(subscription)
	putData := bson.M{}
	json.Unmarshal(tmp, &putData)
	mongoapi.RestfulAPIPost("Subscriptions", bson.M{"subscriptionId": subscription.SubscriptionId}, putData) 
	return putData, nil
}

注意这个函数接受的参数类型是一个models.NrfSubscriptionData,里面就包含了订阅者留下的URINfStatusNotificationUri。可以理解为,订阅者跟NRF说:“当XX事件发生时就call这个NfStatusNotificationUri通知我”。那么需要发消息时,NRF就会构建相应的filter,把所有的这些URI找出来,挨个发一遍消息。

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/management_data.go#L452
func setUriListByFilter(filter bson.M, uriList *[]string) {
	filterNfTypeResultsRaw, err := mongoapi.RestfulAPIGetMany("Subscriptions", filter)
	var filterNfTypeResults []models.NrfSubscriptionData
	for _, subscr := range filterNfTypeResults {
		*uriList = append(*uriList, subscr.NfStatusNotificationUri)
	}
}
点击查看models.NrfSubscriptionData
package models

import (
	"time"
)

type NrfSubscriptionData struct {
	NfStatusNotificationUri string                  `json:"nfStatusNotificationUri" yaml:"nfStatusNotificationUri" bson:"nfStatusNotificationUri" mapstructure:"NfStatusNotificationUri"`
	SubscrCond              interface{}             `json:"subscrCond,omitempty" yaml:"subscrCond" bson:"subscrCond" mapstructure:"SubscrCond"`
	SubscriptionId          string                  `json:"subscriptionId" yaml:"subscriptionId" bson:"subscriptionId" mapstructure:"SubscriptionId"`
	ValidityTime            *time.Time              `json:"validityTime,omitempty" yaml:"validityTime" bson:"validityTime" mapstructure:"ValidityTime"`
	ReqNotifEvents          []NotificationEventType `json:"reqNotifEvents,omitempty" yaml:"reqNotifEvents" bson:"reqNotifEvents" mapstructure:"ReqNotifEvents"`
	PlmnId                  *PlmnId                 `json:"plmnId,omitempty" yaml:"plmnId" bson:"plmnId" mapstructure:"PlmnId"`
	NotifCondition          *NotifCondition         `json:"notifCondition,omitempty" yaml:"notifCondition" bson:"notifCondition" mapstructure:"NotifCondition"`
	ReqNfType               NfType                  `json:"reqNfType,omitempty" yaml:"reqNfType" bson:"reqNfType" mapstructure:"ReqNfType"`
	ReqNfFqdn               string                  `json:"reqNfFqdn,omitempty" yaml:"reqNfFqdn" bson:"reqNfFqdn" mapstructure:"ReqNfFqdn"`
}

点击查看subscription的修改和删除
// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_management.go#L174
func (p *Processor) UpdateSubscriptionProcedure(subscriptionID string, patchJSON []byte) map[string]interface{} {
	collName := "Subscriptions"
	filter := bson.M{"subscriptionId": subscriptionID}

	if err := mongoapi.RestfulAPIJSONPatch(collName, filter, patchJSON); err != nil {
		return nil
	} else {
		if response, err1 := mongoapi.RestfulAPIGetOne(collName, filter); err1 == nil {
			return response
		}
		return nil
	}
}

func (p *Processor) RemoveSubscriptionProcedure(subscriptionID string) {
	collName := "Subscriptions"
	filter := bson.M{"subscriptionId": subscriptionID}

	if err := mongoapi.RestfulAPIDeleteMany(collName, filter); err != nil {
		logger.NfmLog.Errorf("RemoveSubscriptionProcedure err: %+v", err)
	}
}

Nnrf_AccessToken以及OAuth

由于5G网络中每一个NF本质上其实就是一个网络服务,理论上谁都可以访问,我们在命令行使用curl都可以。那么这些NF就需要一个鉴权机制,确保只会回应被授权的实体,而来自其他实体的请求则不回应或直接返回一个403 Forbidden。5G标准选择的鉴权机制是OAuthJWT

  +--------+                                           +---------------+
  |        |--(A)------- Authorization Grant --------->|               |
  |        |                                           |               |
  |        |<-(B)----------- Access Token -------------|               |
  |        |               & Refresh Token             |               |
  |        |                                           |               |
  |        |                            +----------+   |               |
  |        |--(C)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(D)- Protected Resource --| Resource |   | Authorization |
  | Client |                            |  Server  |   |     Server    |
  |        |--(E)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(F)- Invalid Token Error -|          |   |               |
  |        |                            +----------+   |               |
  |        |                                           |               |
  |        |--(G)----------- Refresh Token ----------->|               |
  |        |                                           |               |
  |        |<-(H)----------- Access Token -------------|               |
  +--------+           & Optional Refresh Token        +---------------+

每次NF向另一个NF请求服务,比如说AUSF向UDM请求服务时,AUSF就向NRF要一个JWT,然后用自己的私钥加密这个JWT,设置在请求头中。UDM接收到请求后,使用AUSF的公钥尝试解密请求头中的JWT,成功的话就能确认该请求来自AUSF。在free5gc中,这些公钥和私钥都存放在cert/目录下。对应上图,client就是AUSF,UDM就是Resource Server,而NRF则是Authorization Server。如此,每个收到来自网络中其他NF的请求时,先解码请求头中的JWT,若能解码成功则可以判断这个请求来自网络内部的其他NF,然后根据解码后的JWT信息判断该请求是否有足够的权限(例如一些第三方NF虽然也算是网络内部NF,但无权访问用户隐私数据)。具体来说,每一个NFService都有一个AllowedNfTypes切片,如果它为空,则代表所有NF都可以访问;否则,只有被指定的NF才有权访问。

以下是NRF检查权限并生成JWT的简化版代码,主要是略缺了JWT里具体编码的数据,以及验证NF的网络证书的过程。

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/access_token.go
import (
    "github.com/free5gc/openapi/oauth"
)

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/access_token.go
func (p *Processor) AccessTokenProcedure(request models.AccessTokenReq) (
	*models.AccessTokenRsp, *models.AccessTokenErr,
) {
	errResponse := p.AccessTokenScopeCheck(request)
	if errResponse != nil {
		logger.AccTokenLog.Errorf("AccessTokenScopeCheck error: %v", errResponse.Error)
		return nil, errResponse
	}

	// Create AccessToken
	nrfCtx := nrf_context.GetSelf()
	accessTokenClaims := models.AccessTokenClaims{...}  // omitted

	// Use NRF private key to sign AccessToken
	token := jwt.NewWithClaims(jwt.GetSigningMethod("RS512"), accessTokenClaims)
	accessToken, err := token.SignedString(nrfCtx.NrfPrivKey)

	response := &models.AccessTokenRsp{
		AccessToken: accessToken,
		// ...
	}
	return response, nil
}

func (p *Processor) AccessTokenScopeCheck(req models.AccessTokenReq) *models.AccessTokenErr {
	// Check with nf profile
	collName := nrf_context.NfProfileCollName
	filter := bson.M{"nfInstanceId": reqNfInstanceId}
	consumerNfInfo, err := mongoapi.RestfulAPIGetOne(collName, filter)
	nfProfile := models.NfProfile{}
	mapstruct.Decode(consumerNfInfo, &nfProfile)

	// Verify NF's certificate with root certificate to avoid Man in the Middle Attack
	// more code ...

	// Check scope
	if reqTargetNfType == "NRF" {  // Any NF can access all services of NRF
		return nil
	}
	filter = bson.M{"nfType": reqTargetNfType}
	producerNfInfo, err := mongoapi.RestfulAPIGetOne(collName, filter)
	nfProfile = models.NfProfile{}
	err = mapstruct.Decode(producerNfInfo, &nfProfile)
	nfServices := *nfProfile.NfServices

	scopes := strings.Split(req.Scope, " ")
	for _, reqNfService := range scopes {
		found := false
		for _, nfService := range nfServices {
			if string(nfService.ServiceName) == reqNfService {
				if len(nfService.AllowedNfTypes) == 0 {  // if not specified, any NF can access the reqNfService
					found = true
					break
				} else {
					for _, nfType := range nfService.AllowedNfTypes { // otherwise only the specified NF can access
						if string(nfType) == reqNfType {
							found = true
							break
						}
					}
					break
				}
			}
		}
		if !found {
			logger.AccTokenLog.Errorln("Certificate verify error: Request out of scope (" + reqNfService + ")")
			return &models.AccessTokenErr{
				Error: "invalid_scope",
			}
		}
	}
	return nil
}
点击查看更多数据类型
// https://github.com/free5gc/openapi/blob/449098e08462/models/model_access_token_req.go
type AccessTokenReq struct {
	GrantType           string   `json:"grant_type" yaml:"grant_type" bson:"grant_type" mapstructure:"GrantType"`
	NfInstanceId        string   `json:"nfInstanceId" yaml:"nfInstanceId" bson:"nfInstanceId" mapstructure:"NfInstanceId"`
	NfType              NfType   `json:"nfType,omitempty" yaml:"nfType" bson:"nfType" mapstructure:"NfType"`
	TargetNfType        NfType   `json:"targetNfType,omitempty" yaml:"targetNfType" bson:"targetNfType" mapstructure:"TargetNfType"`
	Scope               string   `json:"scope" yaml:"scope" bson:"scope" mapstructure:"Scope"`
	TargetNfInstanceId  string   `json:"targetNfInstanceId,omitempty" yaml:"targetNfInstanceId" bson:"targetNfInstanceId" mapstructure:"TargetNfInstanceId"`
	RequesterPlmn       *PlmnId  `json:"requesterPlmn,omitempty" yaml:"requesterPlmn" bson:"requesterPlmn" mapstructure:"RequesterPlmn"`
	RequesterPlmnList   []PlmnId `json:"requesterPlmnList,omitempty" yaml:"requesterPlmnList" bson:"requesterPlmnList" mapstructure:"RequesterPlmnList"`
	RequesterSnssaiList []Snssai `json:"requesterSnssaiList,omitempty" yaml:"requesterSnssaiList" bson:"requesterSnssaiList" mapstructure:"RequesterSnssaiList"`
	// Fully Qualified Domain Name
	RequesterFqdn        string      `json:"requesterFqdn,omitempty" yaml:"requesterFqdn" bson:"requesterFqdn" mapstructure:"RequesterFqdn"`
	RequesterSnpnList    []PlmnIdNid `json:"requesterSnpnList,omitempty" yaml:"requesterSnpnList" bson:"requesterSnpnList" mapstructure:"RequesterSnpnList"`
	TargetPlmn           *PlmnId     `json:"targetPlmn,omitempty" yaml:"targetPlmn" bson:"targetPlmn" mapstructure:"TargetPlmn"`
	TargetSnssaiList     []Snssai    `json:"targetSnssaiList,omitempty" yaml:"targetSnssaiList" bson:"targetSnssaiList" mapstructure:"TargetSnssaiList"`
	TargetNsiList        []string    `json:"targetNsiList,omitempty" yaml:"targetNsiList" bson:"targetNsiList" mapstructure:"TargetNsiList"`
	TargetNfSetId        string      `json:"targetNfSetId,omitempty" yaml:"targetNfSetId" bson:"targetNfSetId" mapstructure:"TargetNfSetId"`
	TargetNfServiceSetId string      `` /* 129-byte string literal not displayed */
}

// https://github.com/free5gc/openapi/blob/449098e08462/models/model_access_token_rsp.go
type AccessTokenRsp struct {
	// JWS Compact Serialized representation of JWS signed JSON object (AccessTokenClaims)
	AccessToken string `json:"access_token" yaml:"access_token" bson:"access_token" mapstructure:"AccessToken"`
	TokenType   string `json:"token_type" yaml:"token_type" bson:"token_type" mapstructure:"TokenType"`
	ExpiresIn   int32  `json:"expires_in,omitempty" yaml:"expires_in" bson:"expires_in" mapstructure:"ExpiresIn"`
	Scope       string `json:"scope,omitempty" yaml:"scope" bson:"scope" mapstructure:"Scope"`
}

// https://github.com/free5gc/openapi/blob/v1.0.8/models/model_access_token_claims.go
type AccessTokenClaims struct {
    Iss   string      `json:"iss" yaml:"iss" bson:"iss" mapstructure:"Iss"`
    Sub   string      `json:"sub" yaml:"sub" bson:"sub" mapstructure:"Sub"`
    Aud   interface{} `json:"aud" yaml:"aud" bson:"aud" mapstructure:"Aud"`
    Scope string      `json:"scope" yaml:"scope" bson:"scope" mapstructure:"Scope"`
    Exp   int32       `json:"exp" yaml:"exp" bson:"exp" mapstructure:"Exp"`
    jwt.RegisteredClaims
}

以上是请求方向NRF获得一个JWT的过程和相应的函数。请求方拿到一个JWT后,就把它附在请求头上发出去。收到请求的NF调用free5gc/openapi/auth包里的函数来鉴别其中JWT的权限

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/context/context.go#L206
func (context *NRFContext) AuthorizationCheck(token string, serviceName models.ServiceName) error {
	err := oauth.VerifyOAuth(token, string(serviceName), factory.NrfConfig.GetNrfCertPemPath())
	if err != nil {
		logger.AccTokenLog.Warningln("AuthorizationCheck:", err)
		return err
	}
	return nil
}

虽然这个AuthorizationCheck函数绑定在NRFContext类型上,但在应用鉴权机制时保护的不是Context,而是Router,也就是API接口。每当我们认为访问某一个或某一组API需要权限时,我们就调用NRFContext中的AuthorizationCheck函数:

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/util/router_auth_check.go#L13
type (
	NFContextGetter          func() *nrf_context.NRFContext
	RouterAuthorizationCheck struct {
		serviceName models.ServiceName
	}
)

func NewRouterAuthorizationCheck(serviceName models.ServiceName) *RouterAuthorizationCheck {
	return &RouterAuthorizationCheck{
		serviceName: serviceName,
	}
}

func (rac *RouterAuthorizationCheck) Check(c *gin.Context, nrfContext nrf_context.NFContext) {
	token := c.Request.Header.Get("Authorization")
	err := nrfContext.AuthorizationCheck(token, rac.serviceName)
	if err != nil {
		logger.UtilLog.Debugf("RouterAuthorizationCheck::Check Unauthorized: %s", err.Error())
		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
		c.Abort()
		return
	}
	logger.UtilLog.Debugf("RouterAuthorizationCheck::Check Authorized")
}
// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/server.go#L90
	managementRoutes := s.getNfManagementRoute()
	managementGroup := s.router.Group(factory.NrfNfmResUriPrefix)
	managementAuthCheck := util.NewRouterAuthorizationCheck(models.ServiceName_NNRF_NFM)
	managementGroup.Use(func(c *gin.Context) {
		managementAuthCheck.Check(c, s.Context())
	})
	applyRoutes(managementGroup, managementRoutes)

顺带一提,free5gc把很多OAuth的基本操作封装在了free5gc/openapi中,这样我有点迷惑,毕竟从设计上来说它更应该和mongoapi等其他工具函数一样放在free5gc/util里,而不是与从openapi规范文档中生成的代码放在一起。也许是作者有什么我没有想到的考量吧,但我更倾向于这时候尚未改过来的早期设计缺陷......

NF_Discovery

在free5gc中,NF_Discovery的实现总体上逻辑与NF_Management差不多,都是根据需求构建一个查询,然后让数据库执行这个查询,最后向外界返回查询结果。可以认为,NF_Management做的是NF数据的“增、删、改”,而NF_Discovery做的就是最后的“查”。

下面简化版的NFDiscoveryProcedure代码,整个函数做的事情就是根据用户的需求,也就是编码在URL的query parameter去构架一个MongoDB filter,然后调用MongoDB的查询引擎去执行这个filter,把查询到的结果稍加转换后返回给用户。

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_discovery.go#L75
func (p *Processor) NFDiscoveryProcedure(c *gin.Context, queryParameters url.Values) {
	// Build Query Filter
	var filter bson.M = buildFilter(queryParameters)

	// Use the filter to find documents
	nfProfilesRaw, err := mongoapi.RestfulAPIGetMany(nrf_context.NfProfileCollName, filter)

	// convert data for response
	var nfProfilesStruct []models.NfProfile
	timedecode.Decode(nfProfilesRaw, &nfProfilesStruct)

	// Build and return SearchResult
	searchResult := &models.SearchResult{
		NfInstances:    nfProfilesStruct,
	}
	c.JSON(http.StatusOK, searchResult)
}

这样看起来,整个对NF做查询的逻辑清晰简洁。然而,internal/sbi/processor/nf_discovery.go整个源码文件有两千多行代码!这是因为NRF需要支持各种各样花里胡哨的查询功能。TS29.510@v18.06的Table 6.2.3.2.3.1-1是个长达20页的表格,描述了需要支持哪些URI query parameters,以及怎样根据这些参数执行查询。因此,“构建filter”就是一件复杂繁琐的工作,两千多行代码中的90%都是在干buildFilter(queryParameters)这一件事情。

// https://github.com/free5gc/nrf/blob/v1.2.5/internal/sbi/processor/nf_discovery.go#L184
func buildFilter(queryParameters url.Values) bson.M {
	// build the filter
	filter := bson.M{
		"$and": []bson.M{},
	}

	// [Query-1] target-nf-type
	targetNfType := queryParameters["target-nf-type"][0]
	if targetNfType != "" {
		targetNfTypeFilter := bson.M{
			"nfType": targetNfType,
		}
		filter["$and"] = append(filter["$and"].([]bson.M), targetNfTypeFilter)
	}

	// [Query-2] request-nf-type
	requesterNfType := queryParameters["requester-nf-type"][0]
	if requesterNfType != "" {
		requesterNfTypeFilter := bson.M{
			"$or": []bson.M{
				{"allowedNfTypes": requesterNfType},
				{"allowedNfTypes": bson.M{
					"$exists": false,
				}},
			},
		}
		filter["$and"] = append(filter["$and"].([]bson.M), requesterNfTypeFilter)
	}

	// =============== 2000+ more lines of code ====================

}

buildFilter函数有1000+行代码,但里面的模式也很简单,就是一步步检查URL的queryParameters有没有某个参数,有的话就处理。其处理本质上也就是把参数里的值转化成能被MongoDB的查询引擎理解的语言,最后把所有这些参数拼接成一个巨大的,用"$and"链接起来的查询语句:

	filter["$and"] = append(filter["$and"].([]bson.M), XXX)

本文记录了NRF的主要功能以及在free5gc中的实现。

posted @ 2024-10-14 20:35  zrq96  阅读(71)  评论(0编辑  收藏  举报