Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions nrfcache/match_filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var matchFilters = MatchFilters{
models.NFTYPE_PCF: MatchPcfProfile,
models.NFTYPE_NSSF: MatchNssfProfile,
models.NFTYPE_UDM: MatchUdmProfile,
models.NFTYPE_UDR: MatchUdrProfile,
models.NFTYPE_AMF: MatchAmfProfile,
}

Expand Down Expand Up @@ -187,9 +188,10 @@ func extractSupiNumber(supi string) string {
func MatchAusfProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.ApiSearchNFInstancesRequest) (bool, error) {
supi := opts.GetSupi()
if supi != nil {
// Unrestricted when no SUPI ranges are declared; see MatchPcfProfile.
if profile.AusfInfo == nil || len(profile.AusfInfo.SupiRanges) == 0 {
logger.NrfcacheLog.Debugf("ausf match failed: no SUPI ranges for %s", profile.NfInstanceId)
return false, nil
logger.NrfcacheLog.Debugf("ausf match successful (unrestricted: no SUPI ranges) for %s", profile.NfInstanceId)
return true, nil
}

matchFound := matchSupiRange(*supi, profile.AusfInfo.SupiRanges)
Expand Down Expand Up @@ -279,9 +281,15 @@ func MatchAmfProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.A
func MatchPcfProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.ApiSearchNFInstancesRequest) (bool, error) {
supi := opts.GetSupi()
if supi != nil {
// A profile declaring no SUPI ranges is unrestricted and serves every
// SUPI. The NRF's own discovery filter encodes this as an $or over
// "a range contains the SUPI" / "supiRanges is null" / "supiRanges is
// absent" (nrf producer/nf_discovery.go, [Query-18] supi). Rationale:
// the cache must select the same profiles as the NRF it caches,
// otherwise a cached lookup and a live discovery disagree.
if profile.PcfInfo == nil || len(profile.PcfInfo.SupiRanges) == 0 {
logger.NrfcacheLog.Infof("pcf match found = false (no SUPI ranges)")
return false, nil
logger.NrfcacheLog.Debugf("pcf match found = true (unrestricted: no SUPI ranges)")
return true, nil
}

matchFound := matchSupiRange(*supi, profile.PcfInfo.SupiRanges)
Expand All @@ -297,9 +305,10 @@ func MatchPcfProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.A
func MatchUdmProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.ApiSearchNFInstancesRequest) (bool, error) {
supi := opts.GetSupi()
if supi != nil {
// Unrestricted when no SUPI ranges are declared; see MatchPcfProfile.
if profile.UdmInfo == nil || len(profile.UdmInfo.GetSupiRanges()) == 0 {
logger.NrfcacheLog.Infof("udm match found = false (no SUPI ranges)")
return false, nil
logger.NrfcacheLog.Debugf("udm match found = true (unrestricted: no SUPI ranges)")
return true, nil
}

matchFound := matchSupiRange(*supi, profile.UdmInfo.GetSupiRanges())
Expand All @@ -311,3 +320,27 @@ func MatchUdmProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.A
logger.NrfcacheLog.Infof("udm match found = true (no SUPI filter)")
return true, nil
}

// MatchUdrProfile selects UDR profiles for a SUPI-filtered discovery.
//
// Without an entry in matchFilters a UDR profile is dropped unconditionally,
// so a cached UDR lookup returns nothing and every UDM/PCF data access falls
// through to a live NRF query. Rationale: UDR is resolved on every subscriber
// data access, which makes it the most frequently discovered NF in the core.
func MatchUdrProfile(profile *models.NFProfileDiscovery, opts Nnrf_NFDiscovery.ApiSearchNFInstancesRequest) (bool, error) {
supi := opts.GetSupi()
if supi == nil {
logger.NrfcacheLog.Debugf("udr match successful (no SUPI filter) for %s", profile.NfInstanceId)
return true, nil
}

// Unrestricted when no SUPI ranges are declared; see MatchPcfProfile.
if profile.UdrInfo == nil || len(profile.UdrInfo.GetSupiRanges()) == 0 {
logger.NrfcacheLog.Debugf("udr match successful (unrestricted: no SUPI ranges) for %s", profile.NfInstanceId)
return true, nil
}

matchFound := matchSupiRange(*supi, profile.UdrInfo.GetSupiRanges())
logger.NrfcacheLog.Debugf("udr match found = %v for %s", matchFound, profile.NfInstanceId)
return matchFound, nil
}
158 changes: 158 additions & 0 deletions nrfcache/nrfcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1147,3 +1147,161 @@ func TestAmfProfileMatching(t *testing.T) {
})
}
}

// TestMatchProfileWithoutSupiRangesIsUnrestricted covers profiles that declare
// no SUPI ranges. Such a profile serves every SUPI, so a SUPI-filtered
// discovery must select it. The NRF's own discovery filter encodes this as an
// $or over "a range contains the SUPI" / "supiRanges is null" / "supiRanges is
// absent"; a matcher that rejected these profiles would make cached lookups
// disagree with a live discovery against the same NRF.
func TestMatchProfileWithoutSupiRangesIsUnrestricted(t *testing.T) {
const supi = "imsi-208930100007500"

testCases := []struct {
profile models.NFProfileDiscovery
matcher MatchFilter
name string
}{
{
name: "udm_info_without_supi_ranges",
matcher: MatchUdmProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "UDM-no-ranges",
NfType: models.NFTYPE_UDM,
UdmInfo: &models.UdmInfo{},
},
},
{
name: "udm_info_absent",
matcher: MatchUdmProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "UDM-no-info",
NfType: models.NFTYPE_UDM,
},
},
{
name: "pcf_info_without_supi_ranges",
matcher: MatchPcfProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "PCF-no-ranges",
NfType: models.NFTYPE_PCF,
PcfInfo: &models.PcfInfo{},
},
},
{
name: "pcf_info_absent",
matcher: MatchPcfProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "PCF-no-info",
NfType: models.NFTYPE_PCF,
},
},
{
name: "ausf_info_without_supi_ranges",
matcher: MatchAusfProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "AUSF-no-ranges",
NfType: models.NFTYPE_AUSF,
AusfInfo: &models.AusfInfo{},
},
},
{
name: "ausf_info_absent",
matcher: MatchAusfProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "AUSF-no-info",
NfType: models.NFTYPE_AUSF,
},
},
{
name: "udr_info_without_supi_ranges",
matcher: MatchUdrProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "UDR-no-ranges",
NfType: models.NFTYPE_UDR,
UdrInfo: &models.UdrInfo{},
},
},
{
name: "udr_info_absent",
matcher: MatchUdrProfile,
profile: models.NFProfileDiscovery{
NfInstanceId: "UDR-no-info",
NfType: models.NFTYPE_UDR,
},
},
}

param := Nnrf_NFDiscovery.ApiSearchNFInstancesRequest{}.Supi(supi)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
match, err := tc.matcher(&tc.profile, param)
if err != nil {
t.Fatalf("matcher returned error: %v", err)
}
if !match {
t.Errorf("profile %s declares no SUPI ranges and must match SUPI %s, got no match",
tc.profile.NfInstanceId, supi)
}
})
}
}

// TestMatchProfileWithSupiRangesStillFiltersOut guards the complementary case:
// once a profile declares ranges, a SUPI outside them must not match.
func TestMatchProfileWithSupiRangesStillFiltersOut(t *testing.T) {
ranges := []models.SupiRange{{Start: openapi.PtrString("100000000000000"), End: openapi.PtrString("100000000000009")}}
param := Nnrf_NFDiscovery.ApiSearchNFInstancesRequest{}.Supi("imsi-208930100007500")

profile := models.NFProfileDiscovery{
NfInstanceId: "UDM-ranged",
NfType: models.NFTYPE_UDM,
UdmInfo: &models.UdmInfo{SupiRanges: ranges},
}
match, err := MatchUdmProfile(&profile, param)
if err != nil {
t.Fatalf("MatchUdmProfile returned error: %v", err)
}
if match {
t.Error("SUPI outside the declared ranges must not match")
}
}

// TestUdrProfileIsSelectableFromCache guards the registration of UDR in
// matchFilters. A profile whose NfType has no registered filter is dropped by
// NrfCache.get, so an unregistered UDR made every cached UDR lookup return
// empty and forced a live NRF query on every subscriber data access.
func TestUdrProfileIsSelectableFromCache(t *testing.T) {
if _, ok := matchFilters[models.NFTYPE_UDR]; !ok {
t.Fatal("UDR has no entry in matchFilters, so cached UDR discovery can never return a profile")
}

profile := models.NFProfileDiscovery{
NfInstanceId: "UDR-1",
NfType: models.NFTYPE_UDR,
UdrInfo: &models.UdrInfo{
SupiRanges: []models.SupiRange{
{Start: openapi.PtrString("208930100007500"), End: openapi.PtrString("208930100007599")},
},
},
}

inRange := Nnrf_NFDiscovery.ApiSearchNFInstancesRequest{}.Supi("imsi-208930100007550")
match, err := MatchUdrProfile(&profile, inRange)
if err != nil {
t.Fatalf("MatchUdrProfile returned error: %v", err)
}
if !match {
t.Error("SUPI inside the declared range must match")
}

outOfRange := Nnrf_NFDiscovery.ApiSearchNFInstancesRequest{}.Supi("imsi-208930100009999")
match, err = MatchUdrProfile(&profile, outOfRange)
if err != nil {
t.Fatalf("MatchUdrProfile returned error: %v", err)
}
if match {
t.Error("SUPI outside the declared range must not match")
}
}
Loading