Free5GC原始碼研究(5) - NRF研究

zrq96發表於2024-10-14

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

Free5GC原始碼研究(5) - NRF研究

具體而言,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就會構建相應的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中的實現。

相關文章