Skip to content

feat: implement Nupf_EventExposureService#95

Open
senselessDog wants to merge 1 commit into
free5gc:mainfrom
senselessDog:feat/Nupf_EventExposureService
Open

feat: implement Nupf_EventExposureService#95
senselessDog wants to merge 1 commit into
free5gc:mainfrom
senselessDog:feat/Nupf_EventExposureService

Conversation

@senselessDog

Copy link
Copy Markdown

Nupf_EventExposure PR README

This PR adds a UPF-side implementation of the Nupf_EventExposure service defined by 3GPP TS 29.564 Release 17.

It allows an NF service consumer to create, modify, and delete event subscriptions over HTTP, while the UPF translates those subscriptions into PFCP monitoring URRs and sends usage notifications back to the subscriber.

Implementation paths:

  • NFs/upf/internal/exposure/
  • NFs/upf/internal/pfcp/
  • NFs/upf/pkg/app/

Contents

  1. What This PR Adds
  2. External API
  3. Internal Interfaces
  4. Data Types
  5. Usage
  6. Testing
  7. Architecture Summary
  8. Design Notes

1. What This PR Adds

  • HTTP endpoints for create, modify, and delete subscription operations under /nupf-ee/v1.
  • Notification delivery to the consumer's eventNotifyUri.
  • PFCP monitoring URR programming for matched PDU sessions.
  • Session matching based on UE IP address, anyUe, and optional DNN.
  • Report routing that separates Exposure monitoring URRs from normal SMF-managed PFCP URRs.
  • Thread-safe session snapshot access for the Exposure service.

The current implementation focuses on user data usage measurement events and periodic reporting, which is the path covered by the tests and example workflow below.

2. External API

According to TS 29.564 clause 6.1.3, base URI: {pfcpAddr}:8090/nupf-ee/v1

2.1 Subscription Management API (Subscriptions Collection)

Method URI Description Spec Section
POST /ee-subscriptions Create new event subscription TS 29.564 §5.2.2.2.2, §6.1.3.2.3.1
DELETE /ee-subscriptions/{subscriptionId} Cancel subscription TS 29.564 §5.2.2.2A, §6.1.3.3.3.1
PATCH /ee-subscriptions/{subscriptionId} Modify subscription TS 29.564 §5.2.2.2.3, §6.1.3.3.3.2

POST /ee-subscriptions

Request Body: CreateEventSubscription

Response 201 Created: CreatedEventSubscription + Location header

Location: {host}/nupf-ee/v1/ee-subscriptions/{subscriptionId}
HTTP Status Description
201 Created Subscription created successfully
400 Bad Request JSON parse failed
404 Not Found No matching PDU session
500 Internal Server Error All URR creation failed

DELETE /ee-subscriptions/{subscriptionId}

Response 204 No Content

HTTP Status Description
204 No Content Unsubscribed successfully
404 Not Found SUBSCRIPTION_NOT_FOUND

PATCH /ee-subscriptions/{subscriptionId}

Request Body: ModifySubscriptionRequest (with new UpfEventSubscription)

Response 200 OK: Updated UpfEventSubscription

2.2 Event Notification (Outbound Notify)

Method URI Description Spec Section
POST {eventNotifyUri} Push event notification to consumer TS 29.564 §5.2.2.3, §6.1.5.2

Request Body: NotificationData
Expected Response: 204 No Content


3. Internal Interfaces

3.1 exposure.PfcpInterface

Defined in internal/exposure/server.go, implemented by pfcp.PfcpServer.

Method Signature Description
AddMonitoringURR (lSeid uint64, urrid uint32, repPeriod time.Duration) error Asynchronously add monitoring URR to the specified session, via monitoringCh in PFCP main loop
RemoveMonitoringURR (lSeid uint64, urrid uint32) error Asynchronously remove monitoring URR, same mechanism

3.2 exposure.NodeInterface

Defined in internal/exposure/server.go, wrapped by pkg/app.NodeAdapter for pfcp.LocalNode.

Method Signature Description
GetAllSessions () []SessionInfo Get a snapshot of all active PFCP sessions (with UE IP, DNN, existing URRIDs)

3.3 report.Handler (pkg/app.ReportMultiplexer)

Implemented in pkg/app/mux.go, acts as the report receiver for the forwarder driver.

Method Description
NotifySessReport(sr report.SessReport) Routes USAReports with URRID ≥ 0xF0000001 to ExposureServer, others to PfcpServer
PopBufPkt(seid uint64, pdrid uint16) Delegates to PfcpServer

3.4 pfcp.PfcpServer Monitoring Methods

Defined in internal/pfcp/pfcp.go.

Method Visibility Description
GetLocalNode() *LocalNode public Returns LocalNode for NodeAdapter
AddMonitoringURR(...) public Asynchronously sends monitoringReq{add:true} to monitoringCh
RemoveMonitoringURR(...) public Asynchronously sends monitoringReq{remove:true} to monitoringCh
doAddMonitoringURR(...) private In PFCP main loop: creates URR IE, updates each PDR's URRID list
doRemoveMonitoringURR(...) private In PFCP main loop: removes URR, updates each PDR

3.5 pfcp.Sess / pfcp.LocalNode Fields and Methods

Defined in internal/pfcp/node.go.

Addition Type Description
PDRInfo.LastIE *ie.IE field Stores last CreatePDR/UpdatePDR raw IE, for reconstructing UpdatePDR (gtp5g NLM_F_REPLACE needs this)
Sess.UeIpAddr net.IP field Extracted from PDR PDI UE-IP-Address IE, for subscription matching
Sess.Dnn string field Extracted from PDR PDI Network-Instance IE
LocalNode.mu sync.RWMutex field Protects sess slice, allows safe concurrent reads from Exposure
LocalNode.GetAllSessions() method RLock-protected session snapshot
extractSessionMetadata(pdrIE) function Reads UE IP and DNN from a PDR IE so Exposure can know which PDU session belongs to which UE

4. Data Types

Defined in internal/exposure/datamodel.go, per TS 29.564 clause 6.1.6.

4.1 Request/Response Types

Type Description Spec Reference
CreateEventSubscription POST request body, contains Subscription UpfEventSubscription §6.1.6.2.14
CreatedEventSubscription POST 201 response body, contains SubscriptionId, Subscription, optional ReportList §6.1.6.2.15
ModifySubscriptionRequest PATCH request body §6.1.3.3.3.2
NotificationData Notify POST body, contains NotifCorrId, NotificationItems §6.1.6.2.2

4.2 Core Subscription Types

Type Key Fields Description Spec Reference
UpfEventSubscription EventNotifyUri, NotifCorrId, EventList, EventMode, UeIpAddress, AnyUe, Dnn Single subscription resource §6.1.6.2.11
UpfEventMode Trigger (required), RepPeriod (seconds), MaxReports Report trigger mode §6.1.6.2.12
UpfEvent Type (required), ImmediateFlag, MeasurementType Single event description §6.1.6.2.13

4.3 Measurement Data Types

Type Description Spec Reference
NotificationItem Single event notification item, includes SourceUeIpAddress, TimeStamp, EventType, UsageData §6.1.6.2.3
UserDataUsageMeasurements Usage measurement result, includes VolumeMeasurement, ThroughputMeasurement §6.1.6.2.5
VolumeMeasurement UL/DL/Total bytes, packet count §6.1.6.2.6
ThroughputMeasurement UL/DL throughput (bit/s) §6.1.6.2.7

4.4 Enum Types

Type Possible Values Spec Reference
EventType USER_DATA_USAGE_MEASURES, USER_DATA_USAGE_TRENDS, QOS_MONITORING, TSC_MNGT_INFO §6.1.6.3.3
UpfEventTrigger ONE_TIME, PERIODIC §6.1.6.3.4
MeasurementType VOLUME, THROUGHPUT §6.1.6.3.5
GranularityOfMeasurement PER_UE, PER_FLOW, PER_APP_ID, AGGREGATE §6.1.6.3.6

4.5 Internal Types

Type Defined In Description
exposure.SessionInfo server.go Session snapshot: LocalID uint64, UeIpAddr net.IP, Dnn string, ExistingURRIDs map[uint32]struct{}
pfcp.monitoringReq pfcp.go Monitoring URR request: add/remove bool, lSeid, urrid, repPeriod, respCh chan error

5. Usage

5.1 Startup

The Exposure service starts automatically with UPF, listening on {pfcp.addr}:8090. No extra config needed.

To register the nupf-ee service to NRF, add to config/upfcfg.yaml:

nrfUri: "http://127.0.0.10:8000"

Prerequisite: There must be at least one active PDU session before creating a subscription. The UPF discovers the target UE via the ueIpAddress field — this is the IP address assigned to the UE at PDU session establishment (e.g., 10.60.0.1 from the 10.60.0.0/24 pool). You can find it in the UPF log or via the free5GC Webconsole.

5.2 Start Local Test Notification Receiver

Before creating a subscription, start the bundled notification receiver so the UPF has a reachable endpoint to POST to:

python3 EventExposure_upf/receiver.py
# Mock Notification Receiver listening on http://127.0.0.1:9000
# Accepts any POST path — use e.g. http://127.0.0.1:9000/notify as eventNotifyUri

The receiver accepts any POST path, responds with 204 No Content, and prints each notification to stdout. Keep this running in a separate terminal.

Use a reachable IP-based URI in test environments so periodic notifications do not fail because of DNS resolution issues.

5.3 Create Subscription (Subscribe)

curl -X POST http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions \
  -H "Content-Type: application/json" \
  -d '{
    "subscription": {
      "eventNotifyUri": "http://127.0.0.1:9000/notify",
      "notifyCorrelationId": "corr-001",
      "eventList": [
        {
          "type": "USER_DATA_USAGE_MEASURES",
          "measurementTypes": ["VOLUME"]
        }
      ],
      "eventReportingMode": {
        "trigger": "PERIODIC",
        "repPeriod": 30
      },
      "nfId": "a1b2c3d4-...",
      "ueIpAddress": "10.60.0.1"
    }
  }'

Success Response (201):

{
  "subscriptionId": "550e8400-e29b-41d4-a716-446655440000",
  "subscription": { "..." }
}

Location header: http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions/550e8400-...

5.4 Cancel Subscription (Unsubscribe)

curl -X DELETE http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions/550e8400-e29b-41d4-a716-446655440000
# HTTP 204 No Content

5.5 Modify Subscription (Modify)

curl -X PATCH http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions/550e8400-... \
  -H "Content-Type: application/json" \
  -d '{ "subscription": { "eventNotifyUri": "http://new-endpoint/notify", "..." } }'
# HTTP 200 OK

5.6 Receive Notification

The subscriber's eventNotifyUri endpoint will receive POST requests in the following format:

{
  "notifyCorrelationId": "corr-001",
  "notificationItems": [
    {
      "eventType": "USER_DATA_USAGE_MEASURES",
      "sourceUeIpAddress": "10.60.0.1",
      "timeStamp": "2026-04-28T10:00:00Z",
      "usageData": [
        {
          "volumeMeasurement": {
            "totalVolume": 1048576,
            "ulVolume": 524288,
            "dlVolume": 524288
          }
        }
      ]
    }
  ]
}

Subscriber should respond with 204 No Content.

When using receiver.py, each notification is printed to the terminal:

--- [NOTIFICATION] POST /notify ---
{
  "notifyCorrelationId": "corr-001",
  "notificationItems": [ ... ]
}
---------------------------------------

5.7 Complete End-to-End Test Workflow

After a UE has established a PDU session (UE IP: 10.60.0.1), the full test flow is:

Step 1 — Start the notification receiver (separate terminal)

python3 EventExposure_upf/receiver.py

Step 2 — Create a subscription

SUB=$(curl -s -X POST http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions \
  -H "Content-Type: application/json" \
  -d '{
    "subscription": {
      "eventNotifyUri": "http://127.0.0.1:9000/notify",
      "notifyCorrelationId": "test-001",
      "eventList": [{"type": "USER_DATA_USAGE_MEASURES", "measurementTypes": ["VOLUME"]}],
      "eventReportingMode": {"trigger": "PERIODIC", "repPeriod": 10},
      "ueIpAddress": "10.60.0.1"
    }
  }')
echo $SUB
SUB_ID=$(echo $SUB | python3 -c "import sys,json; print(json.load(sys.stdin)['subscriptionId'])")
echo "Subscription ID: $SUB_ID"

Step 3 — Generate traffic through the UE (ping or iperf via the UE interface)

# From UE side or uesimtun0:
ping -c 20 8.8.8.8 -I uesimtun0

Step 4 — Wait for periodic notifications (every 10 seconds)

The receiver terminal will print volume measurements. Verify that ulVolume / dlVolume increase as traffic flows.

Step 5 — Delete the subscription

curl -X DELETE http://127.0.0.8:8090/nupf-ee/v1/ee-subscriptions/$SUB_ID
# HTTP 204 No Content

Notifications stop after deletion.


6. Testing

Test file: internal/exposure/server_test.go

6.1 Test List

Test Function Scenario Key Verification
TestCreateSubscription_MatchByUeIP Match single session by UE IP Correctly calls AddMonitoringURR, lSeid/repPeriod correct
TestCreateSubscription_AnyUe anyUe=true, matches all sessions AddMonitoringURR called twice (for two sessions)
TestCreateSubscription_NoMatch UE IP not found in any session Returns 404, does not call AddMonitoringURR
TestDeleteSubscription Normal unsubscribe RemoveMonitoringURR called, lSeid correct
TestDeleteSubscription_NotFound Delete non-existent subscription Returns 404
TestHandleReport_ConsumesMonitoringURR Simulate PFCP USAReport Triggers HTTP POST to eventNotifyUri, verifies notification delivery
TestHTTPCreateSubscription POST /ee-subscriptions HTTP route Returns 201, Location header non-empty, response body contains subscriptionId
TestHTTPDeleteSubscription DELETE /ee-subscriptions/{id} HTTP route Returns 204

6.2 Run Tests

cd NFs/upf
go test ./internal/exposure/... -v

Sample Output:

=== RUN   TestCreateSubscription_MatchByUeIP
--- PASS: TestCreateSubscription_MatchByUeIP (0.00s)
=== RUN   TestCreateSubscription_AnyUe
--- PASS: TestCreateSubscription_AnyUe (0.00s)
=== RUN   TestCreateSubscription_NoMatch
--- PASS: TestCreateSubscription_NoMatch (0.00s)
=== RUN   TestDeleteSubscription
--- PASS: TestDeleteSubscription (0.00s)
=== RUN   TestDeleteSubscription_NotFound
--- PASS: TestDeleteSubscription_NotFound (0.00s)
=== RUN   TestHandleReport_ConsumesMonitoringURR
--- PASS: TestHandleReport_ConsumesMonitoringURR (0.01s)
=== RUN   TestHTTPCreateSubscription
--- PASS: TestHTTPCreateSubscription (0.00s)
=== RUN   TestHTTPDeleteSubscription
--- PASS: TestHTTPDeleteSubscription (0.00s)
PASS
ok  github.com/free5gc/go-upf/internal/exposure

6.3 Full Module Test

cd NFs/upf
go test ./...
# Expected failures: buffnetlink (needs GTP kernel module), pkg/app (needs GTP kernel module)
# These are environment limitations, not code errors
go vet ./...      # Static analysis, no output = pass

7. Architecture Summary

7.1 Subscription Creation Flow

sequenceDiagram
    participant C as NF Service Consumer (NWDAF/SMF)
    participant E as ExposureServer (HTTP :8090)
    participant P as PfcpServer (main goroutine)
    participant K as Kernel / gtp5g

    C->>E: POST /nupf-ee/v1/ee-subscriptions {ueIpAddress: "10.60.0.1", trigger: PERIODIC, repPeriod: 30}
  E->>E: Parse request and find matching PDU session by UE IP / anyUe
    alt Found matching session
    E->>E: Allocate a monitoring URRID (0xF0000001+)
    E->>P: AddMonitoringURR(lSeid, urrid, 30s)
    P->>P: Queue request to monitoringCh
    P->>K: Update URR and linked PDRs
    K-->>P: Success
    P-->>E: Return result
    E->>E: Save subscription record
    E-->>C: 201 Created
    else No matching session
        E-->>C: 404 Not Found
    end
Loading

7.2 Periodic Report and Notification Flow

sequenceDiagram
    participant K as Kernel / gtp5g (perio timer)
    participant D as forwarder.Driver
    participant M as ReportMultiplexer
    participant E as ExposureServer
    participant P as PfcpServer
    participant C as NF Service Consumer

    K->>D: URR PERIO triggered, report USAReport{URRID=0xF0000001, volume=...}
    D->>M: NotifySessReport(SessReport{SEID=1, Reports=[USAReport]})
    M->>M: Check URRID >= 0xF0000001?
    alt Monitoring URR (Exposure)
        M->>E: HandleReport(SessReport with monitoring reports only)
        E->>E: Find subscription: sessionURRs[SEID] == URRID?
        E->>E: buildNotificationItem(): fill UE IP, timestamp, volume data
        E->>C: POST {eventNotifyUri} body: NotificationData
        C-->>E: 204 No Content
    else SMF URR (normal PFCP report)
        M->>P: NotifySessReport(SessReport with SMF reports only)
        P->>P: ServeReport() normal handling
    end
Loading

7.3 Unsubscribe Flow

sequenceDiagram
    participant C as NF Service Consumer
    participant E as ExposureServer
    participant P as PfcpServer
    participant K as Kernel / gtp5g

    C->>E: DELETE /nupf-ee/v1/ee-subscriptions/{subID}
    E->>E: Look up saved {lSeid -> urrid}
    loop For each session's monitoring URR
        E->>P: RemoveMonitoringURR(lSeid, urrid)
        P->>P: Queue request to monitoringCh
        P->>K: Remove URR and unlink it from PDRs
        K-->>P: Success
        P-->>E: Return result
    end
    E-->>C: 204 No Content
Loading

7.4 Architecture Diagram

graph TB
  Consumer[NWDAF / Event Consumer]
  Exposure[ExposureServer\nHTTP :8090]
  Node[LocalNode\nGetAllSessions]
  PFCP[PfcpServer\nmonitoringCh]
  Kernel[gtp5g / Kernel]
  Mux[ReportMultiplexer]
  SMF[SMF]

  Consumer -->|Create / Modify / Delete subscription| Exposure
  Exposure -->|Read active sessions| Node
  Exposure -->|AddMonitoringURR / RemoveMonitoringURR| PFCP
  PFCP -->|Program URR / PDR| Kernel
  Kernel -->|Periodic usage reports| Mux
  Mux -->|Monitoring URRs| Exposure
  Mux -->|Normal SMF URRs| PFCP
  PFCP -->|PFCP Session Report| SMF
  Exposure -->|HTTP notification| Consumer

  style Exposure fill:#e8f5e9,stroke:#4caf50
  style PFCP fill:#fff3e0,stroke:#ff9800
  style Mux fill:#e3f2fd,stroke:#2196f3
Loading

8. Design Notes

Issue Decision Reason
gtp5g PDR update needs all attributes Store PDRInfo.LastIE, rebuild UpdatePDR IE gtp5g uses NLM_F_REPLACE flag, does not support delta update
Multi-goroutine session access AddMonitoringURR via monitoringCh in PFCP main loop PFCP session state can only be modified in main loop, avoids data race
URRID does not conflict with SMF Use high range 0xF0000001+ SMF usually starts from 1; uint32 range is sufficient
Notification does not block PFCP main loop HandleReport outside goroutine, HTTP POST is sync but only in report handler goroutine PFCP main loop does not call HandleReport directly

Copilot AI review requested due to automatic review settings April 28, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an initial UPF-side implementation of the 3GPP TS 29.564 Nupf_EventExposure service, wiring an HTTP server into the UPF and translating subscriptions into PFCP monitoring URRs with notification delivery.

Changes:

  • Introduces internal/exposure (HTTP API, subscription store, notification sender) plus unit tests.
  • Adds PFCP-side support for adding/removing monitoring URRs and session metadata extraction for matching.
  • Wires a report multiplexer and Exposure server startup into the UPF app, plus config/logging updates.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
pkg/factory/config.go Adds nrfUri field and shared constants for Exposure port / monitoring URRID base.
pkg/app/mux.go Introduces ReportMultiplexer and NodeAdapter used by Exposure.
pkg/app/app.go Starts Exposure HTTP server and routes reports through the multiplexer.
internal/pfcp/pfcp.go Adds monitoring URR request channel + add/remove URR programming logic.
internal/pfcp/node.go Adds session metadata extraction and concurrency primitives for session access.
internal/logger/logger.go Adds ExposureLog category.
internal/exposure/server.go Implements subscription CRUD, report handling, and notification sending.
internal/exposure/datamodel.go Adds TS 29.564-aligned request/response/data model types.
internal/exposure/server_test.go Adds tests for subscription CRUD, HTTP routes, and report→notify flow.
go.mod / go.sum Adds github.com/google/uuid dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +402 to +406
// allocateURRID finds an unused URR ID starting from UpfMonitoringURRIDBase.
// The monitoring range is 0xF0000001..0xFFFFFFFF to avoid collisions with SMF-allocated URRs.
const monitoringURRIDBase uint32 = 0xF0000001

func allocateURRID(existing map[uint32]struct{}) uint32 {

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allocateURRID hard-codes monitoringURRIDBase even though pkg/factory/config.go defines UpfMonitoringURRIDBase, and the comment refers to that constant. Reusing the shared constant avoids accidental divergence between allocation and report routing thresholds.

Copilot uses AI. Check for mistakes.
}

// HandleReport is called by the ReportMultiplexer for each SessReport.
// It returns true if the report was fully consumed (belongs to a monitoring URR).

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HandleReport docstring says it returns true only if the report was fully consumed, but the implementation returns true if it consumed any matching USAReport. Either adjust the return semantics to require all sr.Reports be consumed, or update the comment/signature to avoid misleading future callers.

Suggested change
// It returns true if the report was fully consumed (belongs to a monitoring URR).
// It returns true if it handled at least one matching monitoring USAReport.

Copilot uses AI. Check for mistakes.
Comment thread pkg/app/mux.go
Comment on lines +66 to +71
// isMonitoringURR returns true if the URR ID is in the monitoring range.
const monitoringURRIDBase uint32 = 0xF0000001

func isMonitoringURR(urrid uint32) bool {
return urrid >= monitoringURRIDBase
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file defines its own monitoringURRIDBase constant even though pkg/factory/config.go introduces UpfMonitoringURRIDBase. To avoid divergence, consider reusing the shared constant (or centralizing the value in one place) for report routing.

Copilot uses AI. Check for mistakes.
Comment thread internal/pfcp/node.go
Comment on lines +758 to +770
// GetAllSessions returns a snapshot of all active sessions.
// Safe to call from goroutines other than the PfcpServer main loop.
func (n *LocalNode) GetAllSessions() []*Sess {
n.mu.RLock()
defer n.mu.RUnlock()
result := make([]*Sess, 0, len(n.sess))
for _, s := range n.sess {
if s != nil {
result = append(result, s)
}
}
return result
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalNode.GetAllSessions returns a slice of *Sess pointers; after the lock is released, other goroutines (PFCP main loop) can mutate session fields/maps, so callers that read URRIDs/UeIpAddr/Dnn risk data races. Consider returning a deep-copied snapshot struct (only the fields Exposure needs) built under an appropriate lock, or add session-level locking for the fields being read.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +129
s.mu.RLock()
defer s.mu.RUnlock()

consumed := false
for _, rec := range s.subs {
monURRID, ok := rec.sessionURRs[sr.SEID]
if !ok {
continue
}
for _, r := range sr.Reports {
usar, isUSA := r.(report.USAReport)
if !isUSA || usar.URRID != monURRID {
continue
}
// Build and send notification
s.sendNotification(rec, sr.SEID, usar)
consumed = true

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleReport holds s.mu.RLock while iterating subscriptions and can call sendNotification (HTTP POST). Doing network I/O while holding the RWMutex can block subscription create/modify/delete and report handling under load; copy the matched subscription records (or the minimal fields needed for notification) under the lock, release it, then perform HTTP calls outside the critical section.

Copilot uses AI. Check for mistakes.
Comment thread pkg/factory/config.go
Comment on lines +25 to +26
// NrfUri is optional. When set, the UPF registers its Nupf_EventExposure service with the NRF.
NrfUri string `yaml:"nrfUri" valid:"optional"`

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NrfUri is documented as triggering NRF registration, but there is no code in this PR that reads cfg.NrfUri or performs registration (searching the repo shows it is unused). Either implement the registration path or adjust the config field comment/PR description to avoid implying behavior that doesn't exist.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +209
locationURI := fmt.Sprintf("%s/nupf-ee/v1/ee-subscriptions/%s",
c.Request.Host, created.SubscriptionId)

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location header is built as "{host}/nupf-ee/v1/..." which omits the scheme (http/https) and will be an invalid absolute URI for many clients. Consider using c.Request.URL (and proxy headers if applicable) to build a full URL, or return a relative Location ("/nupf-ee/v1/ee-subscriptions/{id}") consistently.

Suggested change
locationURI := fmt.Sprintf("%s/nupf-ee/v1/ee-subscriptions/%s",
c.Request.Host, created.SubscriptionId)
locationURI := fmt.Sprintf("/nupf-ee/v1/ee-subscriptions/%s", created.SubscriptionId)

Copilot uses AI. Check for mistakes.
Comment thread pkg/app/mux.go
Comment on lines +14 to +16
// The exposure server's HandleReport is called first. If it claims the report as
// fully consumed (all reports in the SessReport were monitoring URRs), the PFCP
// server is not notified. Otherwise the PFCP server handles it normally.

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type-level comment says HandleReport is called first and the PFCP server is skipped if the report is fully consumed, but the implementation actually splits sr.Reports by URRID and forwards both subsets unconditionally. Please update the comment (or change the logic) so behavior and documentation stay consistent.

Suggested change
// The exposure server's HandleReport is called first. If it claims the report as
// fully consumed (all reports in the SessReport were monitoring URRs), the PFCP
// server is not notified. Otherwise the PFCP server handles it normally.
// NotifySessReport splits each SessReport by URRID: reports for monitoring URRs
// are forwarded to the exposure server, and all remaining reports are forwarded
// to the PFCP server.

Copilot uses AI. Check for mistakes.
Comment thread go.mod
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go.mod adds github.com/google/uuid but keeps it marked as indirect, even though it is imported directly by internal/exposure/server.go. Running go mod tidy should typically promote direct imports (uuid, and also gin) to non-indirect entries, keeping the module metadata consistent.

Copilot uses AI. Check for mistakes.
Comment thread internal/pfcp/pfcp.go
Comment on lines +466 to +480
// AddMonitoringURR asynchronously creates a monitoring URR on the specified session.
// Safe to call from any goroutine; executes in the PfcpServer main loop.
func (s *PfcpServer) AddMonitoringURR(lSeid uint64, urrid uint32, repPeriod time.Duration) error {
respCh := make(chan error, 1)
s.monitoringCh <- monitoringReq{add: true, lSeid: lSeid, urrid: urrid, repPeriod: repPeriod, respCh: respCh}
return <-respCh
}

// RemoveMonitoringURR asynchronously removes a monitoring URR from the specified session.
// Safe to call from any goroutine; executes in the PfcpServer main loop.
func (s *PfcpServer) RemoveMonitoringURR(lSeid uint64, urrid uint32) error {
respCh := make(chan error, 1)
s.monitoringCh <- monitoringReq{remove: true, lSeid: lSeid, urrid: urrid, respCh: respCh}
return <-respCh
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddMonitoringURR/RemoveMonitoringURR send to monitoringCh and then block waiting for resp. If the PFCP main loop has stopped (e.g., during shutdown) these calls can block indefinitely once the channel buffer fills or with no receiver. Consider guarding with a context/timeout, and/or closing monitoringCh on server shutdown and returning an error when the server is stopped.

Copilot uses AI. Check for mistakes.
@d11nn

d11nn commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

@KASHZKX @DBGR18 PTAL

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +114
func (s *Server) HandleReport(sr report.SessReport) bool {
s.mu.RLock()
defer s.mu.RUnlock()

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleReport holds s.mu.RLock while calling sendNotification, which performs a synchronous HTTP POST. This can block subscription create/modify/delete (writers) and keep the subs map locked during slow/failed network I/O. Copy the needed subscription/session data under the lock, then release the lock before doing any HTTP calls.

Copilot uses AI. Check for mistakes.
Comment thread pkg/app/mux.go
Comment on lines +88 to +92
// Copy URR IDs to avoid race conditions (caller may hold no lock)
urrIDs := make(map[uint32]struct{}, len(s.URRIDs))
for id := range s.URRIDs {
urrIDs[id] = struct{}{}
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeAdapter iterates over s.URRIDs without any synchronization. LocalNode.GetAllSessions only protects the slice, not each Sess’s internal maps, which are mutated in the PFCP main loop (e.g., ApplyCreateURR/ApplyRemoveURR). This can trigger data races under -race. Consider returning a deep-copied snapshot from PFCP (built in the PFCP goroutine or protected by a session-level lock) so URRIDs can be safely read here.

Suggested change
// Copy URR IDs to avoid race conditions (caller may hold no lock)
urrIDs := make(map[uint32]struct{}, len(s.URRIDs))
for id := range s.URRIDs {
urrIDs[id] = struct{}{}
}
// Do not iterate s.URRIDs here: LocalNode.GetAllSessions only protects the
// returned slice, while the session's internal maps may still be mutated by
// the PFCP main loop. Reading the map here would race with those writes.
//
// A full-fidelity fix requires PFCP to expose a synchronized/deep-copied
// snapshot of session state. Until then, return an empty URR-ID set rather
// than performing an unsafe map iteration.
urrIDs := make(map[uint32]struct{})

Copilot uses AI. Check for mistakes.
Comment thread internal/pfcp/pfcp.go
Comment on lines 85 to 90
rcvCh: make(chan ReceivePacket, RECEIVE_CHANNEL_LEN),
srCh: make(chan report.SessReport, REPORT_CHANNEL_LEN),
trToCh: make(chan TransactionTimeout, TRANS_TIMEOUT_CHANNEL_LEN),
monitoringCh: make(chan monitoringReq, MONITORING_CHANNEL_LEN),
recoveryTime: time.Now(),
driver: driver,

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monitoringCh is created for cross-goroutine requests, but there’s no shutdown/closure handling. If PfcpServer.Stop() stops the main loop, subsequent AddMonitoringURR/RemoveMonitoringURR calls can block forever sending to monitoringCh. Consider closing monitoringCh when the server stops and/or making Add/Remove use a non-blocking send/select on a done channel so callers fail fast during shutdown.

Copilot uses AI. Check for mistakes.
Comment on lines +399 to +410
if sub.AnyUe {
result = append(result, sess)
continue
}
if sub.UeIpAddress != nil && sess.UeIpAddr != nil && sub.UeIpAddress.Equal(sess.UeIpAddr) {
result = append(result, sess)
continue
}
if sub.Dnn != "" && sub.Dnn == sess.Dnn {
result = append(result, sess)
continue
}

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matchingSessions currently treats DNN as an alternative match condition (OR) rather than an optional filter. With AnyUe=false, a request that includes a DNN could match sessions even when ueIpAddress doesn’t match (or is omitted), which contradicts the PR description (“UE IP / anyUe, optional DNN”). Consider: AnyUe => match all (then filter by DNN if set); else require UE IP match, and if DNN is set additionally require DNN match.

Suggested change
if sub.AnyUe {
result = append(result, sess)
continue
}
if sub.UeIpAddress != nil && sess.UeIpAddr != nil && sub.UeIpAddress.Equal(sess.UeIpAddr) {
result = append(result, sess)
continue
}
if sub.Dnn != "" && sub.Dnn == sess.Dnn {
result = append(result, sess)
continue
}
matched := false
if sub.AnyUe {
matched = true
} else if sub.UeIpAddress != nil && sess.UeIpAddr != nil && sub.UeIpAddress.Equal(sess.UeIpAddr) {
matched = true
}
if !matched {
continue
}
if sub.Dnn != "" && sub.Dnn != sess.Dnn {
continue
}
result = append(result, sess)

Copilot uses AI. Check for mistakes.
Comment thread internal/pfcp/node.go
return nil, errors.New("DeleteSess: invalid lSeid:0")
}

n.mu.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be better to add defer Unlock at here or a comment to mention that the unlock machanism is at where.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants