Analyzing NF Discovery Requests: Step-by-Step Trace of NFSelect Queries
Note
Author: Che-Wei Lin
Date: 2026/1/21
Introduction
The Network Repository Function (NRF) serves as the service discovery backbone of the 5G Core network, enabling every network function to discover and select peers dynamically. In a Service-Based Architecture (SBA), NFs do not have static configurations for their peers; instead, they rely on the NRF to provide real-time status and endpoint information for available services.
In this article, we'll walk through underlying architecture of the Nnrf_NFDiscovery service, the specific query parameters required for NF selection, and practical methods for capturing SBI traffic using tcpdump.
1. NF Discovery Architecture
The Discovery Endpoint
In free5GC, the NRF exposes the Nnrf_NFDiscovery service at:
GET http://<nrf-ip>:<port>/nnrf-disc/v1/nf-instances
This endpoint is implemented in:
// File: internal/sbi/api_discovery.go
func (s *Server) getNfDiscoveryRoutes() []Route {
return []Route{
{
"SearchNFInstances",
http.MethodGet,
"/nf-instances",
s. HTTPSearchNFInstances,
},
}
}
func (s *Server) HTTPSearchNFInstances(c *gin.Context) {
s. Processor().HandleNFDiscoveryRequest(c, c.Request.URL.Query())
}
Request Flow Overview
┌─────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────┐
│ NF (e.g.│ GET │ NRF SBI │ │ Discovery │ │ MongoDB │
│ AMF) │─────>│ Server │─────>│ Processor │─────>│ Query │
└─────────┘ └─────────────┘ └──────────────┘ └─────────┘
│ │ │ │
│ │ │ │
│ │ │<──────────────────┤
│ │ │ NF Profiles │
│ │<─────────────────────┤ │
│<──────────────────┤ 200 OK + JSON │ │
│ NF Profile List │ │ │
The Core Processing Functions
The discovery request goes through these key functions:
HandleNFDiscoveryRequest- HTTP handler (entry point)validateQueryParameters- Validates mandatory parametersNFDiscoveryProcedure- Main processing logicbuildFilter- Constructs MongoDB filter from query paramsmongoapi.RestfulAPIGetMany- Queries the NfProfile collection
2. Query Parameters
free5GC NRF supports 32+ query parameters for fine-grained NF selection. Here are the most important ones:
Mandatory Parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
target-nf-type |
string | Type of NF to discover | SMF, UDM, PCF |
requester-nf-type |
string | Type of requesting NF | AMF, SMF |
Note: Both parameters are required. Missing either will result in HTTP 400 error with "Loss mandatory parameter".
Common Optional Parameters
| Parameter | Description | Example Query | MongoDB Filter Impact |
|---|---|---|---|
target-plmn-list |
PLMN ID for roaming scenarios | [{"mcc":"208","mnc":"93"}] |
Adds $elemMatch on plmnList |
snssais |
Network slice selection | [{"sst":1,"sd":"010203"}] |
Filters by sNssais array |
dnn |
Data Network Name | internet |
Checks SMF/UPF dnnSmfInfoList |
preferred-locality |
Geographic preference | area1 |
Filters by locality field |
How Parameters Map to MongoDB Filters
Based on actual trace logs from our experiments, the buildFilter() function converts query parameters to BSON filters:
File: internal/sbi/processor/nf_discovery.go
Input Parameters:
target-nf-type=SMF
requester-nf-type=AMF
snssais=[{"sst":1,"sd":"010203"}]
dnn=internet
Generated MongoDB Filter:
map[$and:[
map[nfType: SMF]
map[$or:[
map[allowedNfTypes:AMF]
map[allowedNfTypes:map[$exists:false]]
]]
map[$or:[
map[sNssais: map[$elemMatch: map[sd:010203 sst:1]]]
map[sNssais:map[$exists:false]]
]]
map[smfInfo. sNssaiSmfInfoList: map[$elemMatch:map[dnnSmfInfoList:map[$elemMatch:map[dnn:internet]]]]]
]]
3. Capturing Discovery with tcpdump
Step 1: Setup Environment
Ensure your free5GC deployment is running:
# Start free5GC
cd ~/free5gc
./run. sh
# Verify NRF is running
curl http://127.0.0.10:8000/nnrf-nfm/v1/nf-instances
Step 2: Enable Trace Logging
Before running experiments, modify your NRF configuration:
# File: config/nrfcfg.yaml
logger:
enable: true
level: trace # Change from 'info' to 'trace'
reportCaller: true # Change from 'false' to 'true'
Then restart free5GC to apply changes:
# Stop free5GC
./force_kill.sh
# Restart
./run.sh
Step 3: Start tcpdump
Capture SBI traffic on the NRF interface:
# Capture on loopback (if running locally)
sudo tcpdump -i lo -w nrf-discovery.pcap \
'tcp port 8000 and host 127.0.0.10'
# For Docker deployments, capture on bridge network
sudo tcpdump -i br-<network-id> -w nrf-discovery.pcap \
'tcp port 8000'
Step 4: Trigger a Discovery Request
Simulate an AMF discovering an SMF:
curl -v -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
target-nf-type=SMF&\
requester-nf-type=AMF&\
snssais=[{\"sst\":1,\"sd\": \"010203\"}]&\
dnn=internet" | jq .
Step 5: Analyze the Capture
# Convert to text for analysis
tcpdump -r nrf-discovery.pcap -A | less
# Or use tshark for better HTTP/2 analysis
tshark -r nrf-discovery.pcap -Y "http2" -T fields \
-e http2.header.name -e http2.header.value
Actual Packet Capture Results
Pcap File Statistics:
- Total packets captured: 343
- Capture file size: 53KB
- Discovery requests captured: 12 (including retries)
- Timestamp range: 12:28:30 - 13:57:32
TCP Three-Way Handshake
12:28:30.489378 IP 127.0.0.1.38436 > 127.0.0.10.8000: Flags [S], seq 766027190, win 65495
12:28:30.489408 IP 127.0.0.10.8000 > 127.0.0.1.38436: Flags [S.], seq 2423474686, ack 766027191
12:28:30.489418 IP 127.0.0.1.38436 > 127.0.0.10.8000: Flags [.], ack 1, win 512
Analysis: Connection established in 30 microseconds (489378 → 489408 → 489418)
HTTP Request (SMF Discovery)
12:28:30.489529 IP 127.0.0.1.38436 > 127.0.0.10.8000: Flags [P.], seq 1:291, length 290
GET /nnrf-disc/v1/nf-instances?target-nf-type=SMF&requester-nf-type=AMF&snssais=%5B%7B%22sst%22%3A1%2C%22sd%22%3A%22010203%22%7D%5D&dnn=internet&plmn-id=%7B%22mcc%22%3A%22208%22%2C%22mnc%22%3A%2293%22%7D HTTP/1.1
Host: 127.0.0.10:8000
User-Agent: curl/8.12.1
Accept: application/json
Packet Details:
- Source: 127.0.0.1:38436 (Client)
- Destination: 127.0.0.10:8000 (NRF)
- HTTP Method: GET
- Payload length: 290 bytes
- URL-encoded query parameters (note the
%5B,%7Bencoding)
HTTP Response (200 OK)
12:28:30.493105 IP 127.0.0.10.8000 > 127.0.0.1.38436: Flags [P.], seq 1:1792, length 1791
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 05 Jan 2026 12:28:30 GMT
Content-Length: 1666
{"validityPeriod":100,"nfInstances":[{"nfInstanceId":"343aee23-749e-4835-a8b9-fb9174a162ca","nfType":"SMF","nfStatus":"REGISTERED","plmnList":[{"mcc":"208","mnc":"93"}],"sNssais":[{"sst":1,"sd":"010203"},{"sst":1,"sd":"112233"}],"ipv4Addresses":["127.0.0.2"],"locality":"area1","smfInfo":{"sNssaiSmfInfoList":[{"sNssai":{"sst":1,"sd":"010203"},"dnnSmfInfoList":[{"dnn":"internet"}]},{"sNssai":{"sst":1,"sd":"112233"},"dnnSmfInfoList":[{"dnn":"internet"}]}]},"customInfo":{"oauth2":false},"nfServices":[...]}]}
Response Details:
- HTTP Status: 200 OK
- Content-Type: application/json; charset=utf-8
- Content-Length: 1666 bytes
- Total packet length: 1791 bytes (includes HTTP headers)
Complete Transaction Timeline
| Time (μs) | Event | Packet Size | Direction |
|---|---|---|---|
| 489378 | SYN | 60 bytes | Client → NRF |
| 489408 | SYN-ACK | 60 bytes | NRF → Client |
| 489418 | ACK | 52 bytes | Client → NRF |
| 489529 | GET Request | 290 bytes | Client → NRF |
| 489533 | ACK | 52 bytes | NRF → Client |
| 493105 | HTTP 200 Response | 1791 bytes | NRF → Client |
| 493114 | ACK | 52 bytes | Client → NRF |
| 493211 | FIN | 52 bytes | Client → NRF |
| 493239 | FIN-ACK | 52 bytes | NRF → Client |
| 493253 | ACK | 52 bytes | Client → NRF |
Total Transaction Time: 3.875 milliseconds (from initial SYN to final ACK)
- Connection setup: 40μs
- Request sent: 111μs after connection
- Response received: 3.576ms after request
- Connection teardown: 148μs
Other Discovery Requests in Capture
From analyzing the full pcap file, we captured these distinct discovery requests:
1. GET /nnrf-disc/v1/nf-instances?target-nf-type=SMF&... → HTTP/1.1 200 OK
2. GET /nnrf-disc/v1/nf-instances?target-nf-type=SMF&target-plmn-list=... → HTTP/1.1 200 OK
3. GET /nnrf-disc/v1/nf-instances?target-nf-type=UDM&... → HTTP/1.1 200 OK
4. GET /nnrf-disc/v1/nf-instances?target-nf-type=AUSF&... → HTTP/1.1 200 OK
5. GET /nnrf-disc/v1/nf-instances?requester-nf-type=AMF → HTTP/1.1 400 Bad Request
Key Observation: Request #5 returned HTTP 400 because target-nf-type parameter was missing, demonstrating NRF's parameter validation.
Hex Dump Analysis (Sample)
For deeper packet inspection, here's a hex dump of the HTTP GET request:
0x0030: 9748 10e0 4754 5420 2f6e 6e72 662d 6469 .H..GET./nnrf-di
0x0040: 7363 2f76 312f 6e66 2d69 6e73 7461 6e63 sc/v1/nf-instanc
0x0050: 6573 3f74 6172 6765 742d 6e66 2d74 7970 es?target-nf-typ
0x0060: 653d 534d 4626 7265 7175 6573 7465 722d e=SMF&requester-
0x0070: 6e66 2d74 7970 653d 414d 4626 736e 7373 nf-type=AMF&snss
0x0080: 6169 733d 2535 4225 3742 2532 3273 7374 ais=%5B%7B%22sst
This shows the raw bytes of the HTTP request, with ASCII representation on the right side.
4. Correlating with free5GC NRF Logs
Enable Trace-Level Logging
As shown in Step 2 above, edit your NRF configuration:
# File: config/nrfcfg.yaml
logger:
enable: true
level: trace # Critical for seeing MongoDB queries
reportCaller: true # Shows file paths and line numbers
Key Log Messages to Look For
1. Request Received
2026-01-05T10:30:15.456 [INFO][NRF][Disc] Handle NFDiscoveryRequest
Code Location:
File: /home/ubuntu/free5gc/NFs/nrf/internal/sbi/processor/nf_discovery.go
Function: (*Processor).HandleNFDiscoveryRequest
2. Query Filter Construction
2026-01-05T10:30:15.458 [TRACE][NRF][Disc] Query filter: map[$and:[map[nfType: SMF] map[$or:[map[allowedNfTypes:AMF] map[allowedNfTypes:map[$exists:false]]]] map[$or:[map[sNssais:map[$elemMatch:map[sd:010203 sst: 1]]] map[sNssais: map[$exists:false]]]] map[smfInfo.sNssaiSmfInfoList: map[$elemMatch:map[dnnSmfInfoList:map[$elemMatch:map[dnn: internet]]]]]]]
Code Location:
File: /home/ubuntu/free5gc/NFs/nrf/internal/sbi/processor/nf_discovery. go:131
Function: (*Processor).NFDiscoveryProcedure
This log line shows the exact MongoDB filter being executed.
3. HTTP Response Logging
2026-01-05T10:30:15.462 [INFO][NRF][GIN] | 200 | 127.0.0.18 | GET /nnrf-disc/v1/nf-instances? target-nf-type=SMF&...
Code Location:
File: /home/ubuntu/free5gc/util/logger/logger.go
Function: NewGinWithLogrus.ginToLogrus. func1
Shows the complete HTTP request path, response code, and client IP.
4. Validation Errors
If parameters are invalid:
[WARN][NRF][Disc] Invalid Parameter: Loss mandatory parameter
Returned to client:
{
"title": "Invalid Parameter",
"status": 400,
"cause": "Loss mandatory parameter"
}
This follows RFC 7807 Problem Details format.
Complete Log Analysis Example
Here's a complete trace for an AMF→SMF discovery with all parameters:
2026-01-05T14:23:01.789 [INFO][NRF][GIN] | 200 | 127.0.0.18 | GET /nnrf-disc/v1/nf-instances?target-nf-type=SMF&requester-nf-type=AMF&snssais=[{"sst":1,"sd":"010203"}]&dnn=internet&target-plmn-list=[{"mcc":"208","mnc":"93"}]
2026-01-05T14:23:01.789 [INFO][NRF][Disc][/home/ubuntu/free5gc/NFs/nrf/internal/sbi/processor/nf_discovery.go:26] Handle NFDiscoveryRequest
2026-01-05T14:23:01.789 [TRACE][NRF][Disc][/home/ubuntu/free5gc/NFs/nrf/internal/sbi/processor/nf_discovery.go:131] Query filter: map[$and:[map[nfType:SMF] map[$or:[map[allowedNfTypes:AMF] map[allowedNfTypes:map[$exists:false]]]] map[$or:[map[plmnList:map[$elemMatch:map[mcc:208 mnc:93]]]]] map[$or:[map[sNssais:map[$elemMatch:map[sd:010203 sst: 1]]] map[sNssais:map[$exists:false]]]] map[smfInfo.sNssaiSmfInfoList: map[$elemMatch:map[dnnSmfInfoList:map[$elemMatch:map[dnn: internet]]]]]]]
Analysis of this trace:
| Timestamp | Component | Message | Insight |
|---|---|---|---|
| 14:23:01.789 | GIN | HTTP request logged | Client IP: 127.0.0.18 (AMF), GET request with all params |
| 14:23:01.789 | Disc | HandleNFDiscoveryRequest | Entry point at line 26 |
| 14:23:01.789 | Disc | Query filter | MongoDB filter with 5 conditions: type, authorization, PLMN, slice, DNN |
Total processing time: < 1ms (all logs have same timestamp to millisecond precision)
5. Complete Example - AMF Discovering SMF for PDU Session
Let's trace a realistic scenario: An AMF needs to select an SMF to handle a PDU session establishment for a subscriber.
Scenario Details
- Requested Slice: SST=1, SD=010203
- DNN: internet
- PLMN: MCC=208, MNC=93
- Registered SMF: 343aee23-749e-4835-a8b9-fb9174a162ca at 127.0.0.2
Test Case 1: Basic SMF Discovery
Request:
curl -v -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
target-nf-type=SMF&\
requester-nf-type=AMF&\
snssais=[{\"sst\":1,\"sd\": \"010203\"}]&\
dnn=internet" | jq .
NRF Log Output:
[INFO][NRF][Disc] Handle NFDiscoveryRequest
[TRACE][NRF][Disc] Query filter: map[$and:[map[nfType: SMF] map[$or:[map[allowedNfTypes:AMF] map[allowedNfTypes:map[$exists:false]]]] map[$or:[map[sNssais:map[$elemMatch:map[sd:010203 sst:1]]] map[sNssais:map[$exists:false]]]] map[smfInfo.sNssaiSmfInfoList:map[$elemMatch: map[dnnSmfInfoList:map[$elemMatch:map[dnn:internet]]]]]]]
Response:
{
"validityPeriod": 100,
"nfInstances": [
{
"nfInstanceId": "343aee23-749e-4835-a8b9-fb9174a162ca",
"nfType": "SMF",
"nfStatus": "REGISTERED",
"locality": "area1",
"sNssais": [
{"sst": 1, "sd": "010203"},
{"sst": 1, "sd": "112233"}
]
}
]
}
Analysis:
- Discovered 1 SMF instance
- SMF supports multiple slices (010203 and 112233)
- Locality field present: "area1" (can be used for preferred-locality filtering)
- Validity period: 100 seconds (cache TTL)
Test Case 2: Discovery with PLMN Filter
Request:
curl -v -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
target-nf-type=SMF&\
requester-nf-type=AMF&\
snssais=[{\"sst\":1,\"sd\":\"010203\"}]&\
dnn=internet&\
target-plmn-list=[{\"mcc\":\"208\",\"mnc\":\"93\"}]" | jq .
NRF MongoDB Filter:
map[$and:[
map[nfType:SMF]
map[$or:[map[allowedNfTypes:AMF] map[allowedNfTypes: map[$exists:false]]]]
map[$or:[map[plmnList:map[$elemMatch: map[mcc:208 mnc:93]]]]] // ← Added PLMN filter
map[$or:[map[sNssais:map[$elemMatch:map[sd:010203 sst: 1]]] map[sNssais:map[$exists:false]]]]
map[smfInfo.sNssaiSmfInfoList:map[$elemMatch:map[dnnSmfInfoList:map[$elemMatch:map[dnn:internet]]]]]
]]
Response:
{
"validityPeriod": 100,
"nfInstances": [
{
"nfInstanceId": "343aee23-749e-4835-a8b9-fb9174a162ca",
"nfType": "SMF",
"ipv4Addresses": ["127.0.0.2"] // ← IP address included
}
]
}
Analysis:
- Same SMF discovered (PLMN matches)
- Response now includes
ipv4Addressesarray - Additional filter condition added (PLMN list matching)
Test Case 3: Discovering Other NF Types
UDM Discovery:
curl -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
target-nf-type=UDM&\
requester-nf-type=AMF" | jq .
MongoDB Filter:
map[$and:[
map[nfType:UDM]
map[$or:[map[allowedNfTypes:AMF] map[allowedNfTypes: map[$exists:false]]]]
]]
Response:
{
"validityPeriod": 100,
"nfInstances": [
{
"nfInstanceId": "ed470d29-524f-4beb-9c15-dfa35cb68924",
"nfType": "UDM",
"nfStatus": "REGISTERED"
}
]
}
AUSF Discovery:
curl -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
target-nf-type=AUSF&\
requester-nf-type=AMF" | jq .
Response:
{
"validityPeriod": 100,
"nfInstances": [
{
"nfInstanceId": "23585e14-1f66-4276-8c6e-8ead28de840c",
"nfType": "AUSF",
"nfStatus": "REGISTERED",
"ipv4Addresses": ["127.0.0.9"]
}
]
}
Analysis:
- UDM and AUSF discoveries are simpler (no slice/DNN filtering needed)
- Only type and authorization filters applied
- Both returned successfully with NF instance IDs and status
Test Case 4: Error Handling - Missing Mandatory Parameter
Request (intentionally missing target-nf-type):
curl -X GET "http://127.0.0.10:8000/nnrf-disc/v1/nf-instances?\
requester-nf-type=AMF" | jq .
Response:
{
"title": "Invalid Parameter",
"status": 400,
"cause": "Loss mandatory parameter"
}
HTTP Status: 400 Bad Request
Analysis:
- Validation happens before database query
- Returns RFC 7807 Problem Details format
- No MongoDB query executed
MongoDB Verification
You can manually verify the discovered NF profiles in MongoDB:
# Connect to MongoDB
mongosh mongodb://127.0.0.1:27017/free5gc
# Query SMF profiles
db.NfProfile.find({
"nfType": "SMF",
"nfStatus": "REGISTERED",
"sNssais": { "$elemMatch": { "sst": 1, "sd": "010203" } }
}).pretty()
Expected Output:
{
"_id": ObjectId("..."),
"nfInstanceId": "343aee23-749e-4835-a8b9-fb9174a162ca",
"nfType": "SMF",
"nfStatus": "REGISTERED",
"heartBeatTimer": 60,
"plmnList": [
{"mcc": "208", "mnc": "93"}
],
"sNssais": [
{"sst": 1, "sd": "010203"},
{"sst": 1, "sd": "112233"}
],
"ipv4Addresses": ["127.0.0.2"],
"locality": "area1",
"smfInfo": {
"sNssaiSmfInfoList": [
{
"sNssai": {"sst": 1, "sd": "010203"},
"dnnSmfInfoList": [
{"dnn": "internet"}
]
}
]
}
}
Conclusion
In a 5G Service Based Architecture, NF Discovery serves as the critical control mechanism that transforms a collection of isolated microservices into a synchronized, high-performance network. By leveraging the Network Repository Function (NRF) as a centralized service registry, the core evolves beyond static, hard-coded peer-to-peer configurations into a programmable environment. This architecture ensures that service consumers can dynamically bind to the most optimal service producer instance available.
This analysis shows how free5GC turns high-level NF discovery requests into concrete MongoDB queries inside the NRF discovery pipeline. By enabling trace-level logging, you can clearly see the exact filter conditions used during NF selection, making the discovery process much more transparent.
Reference
About
Hello! I'm Che Wei, Lin. I’ve recently begun my journey into 5G technology and the free5GC community. I hope you found this blog post helpful, and please feel free to reach out for further discussion.
Connect with Me
- GitHub: Zach1113