Conversation
Create cdacmaster.yml Define CI pipeline with multiple jobs and permissions This workflow defines a CI pipeline with multiple jobs for building, testing, and analyzing the codebase. Each job has specific permissions and uses reusable workflows from the omec-project repository. Update cdacmaster.yml
d31832a to
6fda748
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces IMS-related PDU session modification support in the SMF, including P-CSCF address handling and policy-driven QoS updates that trigger PFCP session modification plus NAS/NGAP modification messaging.
Changes:
- Add UE-facing QoS update handling (QoS rules/flows + N1N2 PDU Session Resource Modify) for SM Policy update notifications.
- Extend PFCP Session Modification to include rule removal (Remove PDR/FAR/QER) and update call sites/tests accordingly.
- Add configurable P-CSCF IPv4/IPv6 info and attempt to include P-CSCF IPv4 in PCO when requested.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| util/qos_convert.go | Adds bitrate normalization helper used when building QER rates. |
| qos/traffic_control_data.go | Implements concrete change detection for TrafficControlData. |
| qos/qos_rule.go | Adds QoS rule add/modify/delete building helpers and updates marshaling for delete rules. |
| qos/qos_flow.go | Adds QoS flow add/modify/delete description building and QoS data change detection. |
| qos/pcc_rule.go | Implements PCC rule diffing helper logic. |
| producer/ulcl_procedure.go | Updates PFCP session modification calls to new signature. |
| producer/sm_pfcp_handling.go | Updates PFCP session modification call to new signature. |
| producer/pdu_session.go | Updates PFCP session modification call to new signature. |
| producer/n1n2_data_handler.go | Extends PFCP param struct with removal lists. |
| producer/datapath.go | Updates PFCP session modification call to new signature. |
| producer/callback.go | Implements SM Policy Update Notify handling to drive PFCP modify + N1N2 QoS modification. |
| pfcp/message/send.go | Adds remove PDR/FAR/QER parameters to PFCP session modification send path. |
| pfcp/message/build.go | Adds Remove PDR/FAR/QER IEs to PFCP Session Modification Request. |
| pfcp/message/send_test.go | Updates PFCP send test for new signature. |
| pfcp/message/build_test.go | Updates PFCP build tests for new signature. |
| factory/config.go | Adds PCSCFInfo configuration struct + YAML field. |
| config/smfcfg.yaml | Adds example pcscfInfos config values. |
| context/context.go | Plumbs configured PCSCFInfo into SMF context. |
| context/pcscf_info.go | Adds context-level PCSCFInfo type. |
| context/pco.go | Adds protocol option flag for PCSCF IPv4 request. |
| context/gsm_handler.go | Attempts to detect UE PCSCF request from PCO container list. |
| context/gsm_build.go | Attempts to add PCSCF IPv4 address into EPCO and builds PDU Session Modification Command. |
| context/ngap_build.go | Adds PDUSessionResourceModifyRequestTransfer builder + bitrate parsing helper. |
| context/sm_context.go | Adds additional logging and null-guards in SelectedSessionRule. |
| context/datapath.go | Adds dedicated QER creation (GBR/MBR) for non-default QoS flows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func GetPfId(pfID string) uint8 { | ||
| if pfID == "" { | ||
| fmt.Println("Warning: PackFiltId is empty, defaulting to 0") | ||
| return 0 | ||
| } | ||
| id, err := strconv.Atoi(pfID) | ||
| if err != nil { | ||
| fmt.Printf("Error converting PackFiltId [%s] to int: %v. Defaulting to 0\n", pfID, err) | ||
| return 0 | ||
| } else { | ||
| return (uint8(id) & PacketFilterIdBitmask) | ||
| } | ||
| return uint8(id) | ||
| } |
There was a problem hiding this comment.
GetPfId now prints warnings/errors to stdout via fmt and returns uint8(id) without applying PacketFilterIdBitmask (IDs are limited to 0-15 per the struct comment). This can both spam stdout and produce invalid packet filter identifiers. Prefer logger.QosLog and return (uint8(id) & PacketFilterIdBitmask).
| qosFlowUpdate := smPolicyUpdates.QosFlowUpdate | ||
| hasUpdates := false // Track if any QoS flows were processed | ||
|
|
||
| // =============================== | ||
| // Handle PCC rule deletions if QoS flow updates are nil | ||
| // =============================== | ||
| if smPolicyUpdates == nil || smPolicyUpdates.QosFlowUpdate == nil { | ||
| logger.QosLog.Warn("smPolicyUpdates or QosFlowUpdate is nil, processing PCC rule deletions only") | ||
|
|
||
| for pccRuleID := range smPolicyUpdates.PccRuleUpdate.del { | ||
| logger.QosLog.Infof("Processing deletion for PCC rule ID: %s", pccRuleID) | ||
|
|
||
| qfiVal, err := strconv.Atoi(pccRuleID) | ||
| if err != nil { | ||
| logger.QosLog.Errorf("Invalid QFI string for PCC rule ID '%s': %v", pccRuleID, err) | ||
| continue | ||
| } | ||
| qfi := uint8(qfiVal) | ||
|
|
||
| logger.QosLog.Infof("Deleting QoS Flow Description for QFI=%d (from PCC rule %s)", qfi, pccRuleID) | ||
|
|
||
| // Skip if QFI is zero | ||
| if qfi == 0 { | ||
| logger.QosLog.Warnf("Skipping QoS Flow deletion because QFI=0 for PCC rule ID='%s'", pccRuleID) | ||
| continue | ||
| } | ||
|
|
||
| // Build delete QoS Flow Description | ||
| QFDescriptions.BuildDelQosFlowDescFromQoSDesc(qfi) | ||
| hasUpdates = true | ||
| } | ||
|
|
||
| if hasUpdates { | ||
| logger.QosLog.Infof("Completed building delete QoS flow descriptions for %d PCC rules", len(smPolicyUpdates.PccRuleUpdate.del)) | ||
| } else { | ||
| logger.QosLog.Warn("No QoS flow deletions were processed") | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
BuildAuthorizedQosFlowDescriptions assigns qosFlowUpdate := smPolicyUpdates.QosFlowUpdate before checking smPolicyUpdates for nil, and then inside the nil-check block it still dereferences smPolicyUpdates (e.g., smPolicyUpdates.PccRuleUpdate.del). If smPolicyUpdates is nil this will panic. The function should return early when smPolicyUpdates is nil, and separately guard PccRuleUpdate/del from nil before ranging.
| qosFlowUpdate := smPolicyUpdates.QosFlowUpdate | |
| hasUpdates := false // Track if any QoS flows were processed | |
| // =============================== | |
| // Handle PCC rule deletions if QoS flow updates are nil | |
| // =============================== | |
| if smPolicyUpdates == nil || smPolicyUpdates.QosFlowUpdate == nil { | |
| logger.QosLog.Warn("smPolicyUpdates or QosFlowUpdate is nil, processing PCC rule deletions only") | |
| for pccRuleID := range smPolicyUpdates.PccRuleUpdate.del { | |
| logger.QosLog.Infof("Processing deletion for PCC rule ID: %s", pccRuleID) | |
| qfiVal, err := strconv.Atoi(pccRuleID) | |
| if err != nil { | |
| logger.QosLog.Errorf("Invalid QFI string for PCC rule ID '%s': %v", pccRuleID, err) | |
| continue | |
| } | |
| qfi := uint8(qfiVal) | |
| logger.QosLog.Infof("Deleting QoS Flow Description for QFI=%d (from PCC rule %s)", qfi, pccRuleID) | |
| // Skip if QFI is zero | |
| if qfi == 0 { | |
| logger.QosLog.Warnf("Skipping QoS Flow deletion because QFI=0 for PCC rule ID='%s'", pccRuleID) | |
| continue | |
| } | |
| // Build delete QoS Flow Description | |
| QFDescriptions.BuildDelQosFlowDescFromQoSDesc(qfi) | |
| hasUpdates = true | |
| } | |
| if hasUpdates { | |
| logger.QosLog.Infof("Completed building delete QoS flow descriptions for %d PCC rules", len(smPolicyUpdates.PccRuleUpdate.del)) | |
| } else { | |
| logger.QosLog.Warn("No QoS flow deletions were processed") | |
| } | |
| } | |
| hasUpdates := false // Track if any QoS flows were processed | |
| // =============================== | |
| // Handle nil smPolicyUpdates early | |
| // =============================== | |
| if smPolicyUpdates == nil { | |
| logger.QosLog.Warn("smPolicyUpdates is nil, nothing to process") | |
| return &QFDescriptions | |
| } | |
| // =============================== | |
| // Handle PCC rule deletions if QoS flow updates are nil | |
| // =============================== | |
| if smPolicyUpdates.QosFlowUpdate == nil { | |
| logger.QosLog.Warn("QosFlowUpdate is nil, processing PCC rule deletions only") | |
| if smPolicyUpdates.PccRuleUpdate != nil && smPolicyUpdates.PccRuleUpdate.del != nil { | |
| for pccRuleID := range smPolicyUpdates.PccRuleUpdate.del { | |
| logger.QosLog.Infof("Processing deletion for PCC rule ID: %s", pccRuleID) | |
| qfiVal, err := strconv.Atoi(pccRuleID) | |
| if err != nil { | |
| logger.QosLog.Errorf("Invalid QFI string for PCC rule ID '%s': %v", pccRuleID, err) | |
| continue | |
| } | |
| qfi := uint8(qfiVal) | |
| logger.QosLog.Infof("Deleting QoS Flow Description for QFI=%d (from PCC rule %s)", qfi, pccRuleID) | |
| // Skip if QFI is zero | |
| if qfi == 0 { | |
| logger.QosLog.Warnf("Skipping QoS Flow deletion because QFI=0 for PCC rule ID='%s'", pccRuleID) | |
| continue | |
| } | |
| // Build delete QoS Flow Description | |
| QFDescriptions.BuildDelQosFlowDescFromQoSDesc(qfi) | |
| hasUpdates = true | |
| } | |
| if hasUpdates { | |
| logger.QosLog.Infof("Completed building delete QoS flow descriptions for %d PCC rules", len(smPolicyUpdates.PccRuleUpdate.del)) | |
| } else { | |
| logger.QosLog.Warn("No QoS flow deletions were processed") | |
| } | |
| } else { | |
| logger.QosLog.Warn("PccRuleUpdate or its delete map is nil, no QoS flow deletions to process") | |
| } | |
| // No QoS flow updates to process beyond deletions | |
| return &QFDescriptions | |
| } | |
| qosFlowUpdate := smPolicyUpdates.QosFlowUpdate |
| smContext.SMLock.Lock() | ||
| defer smContext.SMLock.Unlock() | ||
|
|
||
| logger.PduSessLog.Infoln("In HandleSMPolicyUpdateNotify") | ||
| pcfPolicyDecision := request.SmPolicyDecision | ||
|
|
||
| if smContext.SMContextState != smfContext.SmStateActive { | ||
| // Wait till the state becomes SmStateActive again | ||
| // TODO: implement waiting in concurrent architecture | ||
| logger.PduSessLog.Warnf("SMContext[%s-%02d] should be SmStateActive, but actual %s", | ||
| smContext.Supi, smContext.PDUSessionID, smContext.SMContextState.String()) | ||
| } | ||
|
|
||
| //TODO: Response data type - | ||
| //[200 OK] UeCampingRep | ||
| //[200 OK] array(PartialSuccessReport) | ||
| //[400 Bad Request] ErrorReport | ||
| // Derive QoS change | ||
| logger.PduSessLog.Infof("Building SM Policy Update for UE [%s], PDU Session ID [%d]", | ||
| smContext.Supi, smContext.PDUSessionID) | ||
|
|
||
| // Derive QoS change(compare existing vs received Policy Decision) | ||
| policyUpdates := qos.BuildSmPolicyUpdate(&smContext.SmPolicyData, pcfPolicyDecision) | ||
| smContext.SmPolicyUpdates = append(smContext.SmPolicyUpdates, policyUpdates) | ||
|
|
||
| // Update UPF | ||
| // TODO | ||
| logger.PduSessLog.Infof("SM Policy Update built: %+v", policyUpdates) | ||
|
|
||
| httpResponse := httpwrapper.NewResponse(http.StatusNoContent, nil, nil) | ||
| txn.Rsp = httpResponse | ||
| smContext.SmPolicyUpdates = append(smContext.SmPolicyUpdates[:0], policyUpdates) | ||
| logger.PduSessLog.Infof("Appended SM Policy Update, total updates count: %d", | ||
| len(smContext.SmPolicyUpdates)) | ||
| logger.PduSessLog.Infof("SmPolicyUpdates: %v", smContext.SmPolicyUpdates) | ||
|
|
||
| // Build PFCP parameters | ||
| pfcpParam := BuildPfcpParam(smContext) | ||
| // Set state to PFCP Modify before sending PFCP request | ||
| smContext.ChangeState(smfContext.SmStatePfcpModify) | ||
|
|
||
| if err := SendPfcpSessionModifyReq(smContext, pfcpParam); err != nil { |
There was a problem hiding this comment.
HandleSMPolicyUpdateNotify holds smContext.SMLock across PFCP signaling and the AMF N1N2 call (network I/O + waiting on SBIPFCPCommunicationChan). This can block unrelated SM context operations and may deadlock in the UPF-adapter path where PFCP response handlers also take SMLock. Consider limiting the lock scope to only the shared-state mutations, releasing it before sending PFCP/HTTP requests and waiting for responses.
| // ---------------------- | ||
| // Handle Downlink PDRs | ||
| // ---------------------- | ||
| if dlPDR, ok := ANUPF.DownLinkTunnel.PDR[ruleid]; ok { |
There was a problem hiding this comment.
BuildPfcpParam selects a single PCC rule key (ruleid) from the SmPolicyDecision.PccRules map and only processes PDRs with that key. If multiple PCC rules/PDRs exist, the rest will be ignored (and in release-only case, only one rule would be removed). This should iterate over all relevant PDR entries (or all keys in the PDR maps) rather than picking one arbitrary map key.
| if dlPDR, ok := ANUPF.DownLinkTunnel.PDR[ruleid]; ok { | |
| for ruleid, dlPDR := range ANUPF.DownLinkTunnel.PDR { |
| for pccRuleName, pccRuleVal := range pccRulesUpdate.mod { | ||
| logger.QosLog.Infof("building QoS Rule from modified PCC rule [%s]", pccRuleName) | ||
| refQosData := GetQoSDataFromPolicyDecision(smPolicyDecision, pccRuleVal.RefQosData[0]) | ||
| qosRule := BuildAddQoSRuleFromPccRule(pccRuleVal, refQosData, OperationCodeCreateNewQoSRule) |
There was a problem hiding this comment.
BuildQosRules treats PCC rule modifications as creates (OperationCodeCreateNewQoSRule). For existing rule updates this should use a modify operation code (and likely BuildModifyQosRuleFromPccRule) so the UE applies it as a modification rather than a new rule.
| qosRule := BuildAddQoSRuleFromPccRule(pccRuleVal, refQosData, OperationCodeCreateNewQoSRule) | |
| qosRule := BuildAddQoSRuleFromPccRule(pccRuleVal, refQosData, OperationCodeModifyExistingQoSRuleAndReplaceAllPacketFilters) |
| ANUPF := defaultPath.FirstDPNode | ||
| err := pfcp_message.SendPfcpSessionModificationRequest(ANUPF.UPF.NodeID, smContext, | ||
| pfcpParam.pdrList, pfcpParam.farList, pfcpParam.barList, pfcpParam.qerList, ANUPF.UPF.Port) | ||
| pfcpParam.pdrList, pfcpParam.farList, pfcpParam.barList, pfcpParam.qerList, nil, nil, nil, ANUPF.UPF.Port) |
There was a problem hiding this comment.
SendPfcpSessionModifyReq always passes nil removePDR/removeFAR/removeQER into SendPfcpSessionModificationRequest. This makes the new teardown/removal lists in pfcpParam ineffective (e.g., BuildPfcpParam's release-only path). Pass pfcpParam.removePDR/removeFAR/removeQER through so PFCP removal IEs are actually sent.
| pfcpParam.pdrList, pfcpParam.farList, pfcpParam.barList, pfcpParam.qerList, nil, nil, nil, ANUPF.UPF.Port) | |
| pfcpParam.pdrList, pfcpParam.farList, pfcpParam.barList, pfcpParam.qerList, pfcpParam.removePDR, pfcpParam.removeFAR, pfcpParam.removeQER, ANUPF.UPF.Port) |
| case nasMessage.PCSCFIPv4AddressRequestUL: | ||
| smContext.SubGsmLog.Infoln("Didn't Implement container type PCSCFIPv4AddressRequestUL") | ||
| case nasMessage.DNSServerIPv4AddressRequestUL: | ||
| smContext.ProtocolConfigurationOptions.DNSIPv4Request = true | ||
| case nasMessage.MSISDNRequestUL: | ||
| smContext.SubGsmLog.Infoln("Didn't Implement container type MSISDNRequestUL") | ||
| case nasMessage.IFOMSupportRequestUL: | ||
| smContext.SubGsmLog.Infoln("Didn't Implement container type IFOMSupportRequestUL") | ||
| case nasMessage.MSSupportOfLocalAddressInTFTIndicatorUL: | ||
| smContext.SubGsmLog.Infoln("Didn't Implement container type MSSupportOfLocalAddressInTFTIndicatorUL") | ||
| case nasMessage.PCSCFReSelectionSupportUL: | ||
| smContext.SubGsmLog.Infoln("Didn't Implement container type PCSCFReSelectionSupportUL") | ||
| smContext.ProtocolConfigurationOptions.PCSCFIPv4Request = true | ||
| smContext.SubGsmLog.Infoln("PCSCFIPv4AddressRequestUL has been set true") |
There was a problem hiding this comment.
The PCSCF IPv4 request flag is set when encountering PCSCFReSelectionSupportUL, but the actual container ID for requesting a P-CSCF IPv4 address is PCSCFIPv4AddressRequestUL. As written, a UE that sends PCSCFIPv4AddressRequestUL will still hit the "Didn't Implement" case and not set PCSCFIPv4Request.
| val, err := strconv.ParseUint(strings.TrimSuffix(s, "kbps"), 10, 64) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return val * 1000, nil | ||
| } | ||
| if strings.HasSuffix(s, "mbps") { | ||
| val, err := strconv.ParseUint(strings.TrimSuffix(s, "mbps"), 10, 64) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| return val * 1000 * 1000, nil | ||
| } | ||
| return 0, nil |
There was a problem hiding this comment.
StringToBitRate returns (0, nil) for unsupported/invalid strings and does not trim whitespace after stripping the unit (e.g., "101 Mbps" -> trimSuffix("mbps") yields "101 " which ParseUint fails). This can silently convert rates to 0 and mask errors. Consider trimming spaces after TrimSuffix and returning a non-nil error for unknown/invalid units so callers can handle it explicitly.
| val, err := strconv.ParseUint(strings.TrimSuffix(s, "kbps"), 10, 64) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return val * 1000, nil | |
| } | |
| if strings.HasSuffix(s, "mbps") { | |
| val, err := strconv.ParseUint(strings.TrimSuffix(s, "mbps"), 10, 64) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return val * 1000 * 1000, nil | |
| } | |
| return 0, nil | |
| numPart := strings.TrimSpace(strings.TrimSuffix(s, "kbps")) | |
| val, err := strconv.ParseUint(numPart, 10, 64) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return val * 1000, nil | |
| } | |
| if strings.HasSuffix(s, "mbps") { | |
| numPart := strings.TrimSpace(strings.TrimSuffix(s, "mbps")) | |
| val, err := strconv.ParseUint(numPart, 10, 64) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return val * 1000 * 1000, nil | |
| } | |
| return 0, fmt.Errorf("unsupported bitrate format: %q", s) |
| // Set Guaranteed Bit Rate (GBR) if configured | ||
| if qosData.GbrUl != "" && qosData.GbrDl != "" { | ||
| newQER.GBR = &GBR{ | ||
| ULGBR: util.BitRateTokbps(util.NormalizeBitRate(qosData.GbrUl)), | ||
| DLGBR: util.BitRateTokbps(util.NormalizeBitRate(qosData.GbrDl)), | ||
| } | ||
| logger.PduSessLog.Infof("CreateDedicatedQosQer: GBR set [UL=%d kbps, DL=%d kbps]", | ||
| newQER.GBR.ULGBR, newQER.GBR.DLGBR) | ||
| } else { | ||
| logger.PduSessLog.Infof("CreateDedicatedQosQer: no GBR configured for QoSId [%s]", qosData.QosId) | ||
| } | ||
|
|
||
| // Set Maximum Bit Rate (MBR) if configured | ||
| if qosData.MaxbrUl != "" && qosData.MaxbrDl != "" { | ||
| newQER.MBR = &MBR{ | ||
| ULMBR: util.BitRateTokbps(util.NormalizeBitRate(qosData.MaxbrUl)), | ||
| DLMBR: util.BitRateTokbps(util.NormalizeBitRate(qosData.MaxbrDl)), | ||
| } |
There was a problem hiding this comment.
CreateDedicatedQosQer normalizes rates via util.NormalizeBitRate before BitRateTokbps. NormalizeBitRate currently hardcodes the unit to Kbps when a decimal is present, which will mis-handle values like "101.5 Mbps" and can drastically under-state configured GBR/MBR. Consider normalizing by trimming fractional zeros while preserving the original unit token, or updating BitRateTokbps to parse floats directly.
| parts := strings.Split(br, ".") | ||
| if len(parts) > 1 { | ||
| br = parts[0] + " Kbps" | ||
| } |
There was a problem hiding this comment.
NormalizeBitRate rewrites any value containing a decimal point to " Kbps", which discards the original unit (e.g., "1.5 Mbps" becomes "1 Kbps") and can under/over-state rates. Consider parsing the numeric part (float) and preserving the original unit token (bps/Kbps/Mbps/...) when normalizing, or normalizing by trimming trailing zeros without hardcoding a unit.
| parts := strings.Split(br, ".") | |
| if len(parts) > 1 { | |
| br = parts[0] + " Kbps" | |
| } | |
| // Normalize only the numeric part and preserve the original unit token(s). | |
| fields := strings.Fields(br) | |
| if len(fields) == 0 { | |
| return strings.TrimSpace(br) | |
| } | |
| numeric := fields[0] | |
| unit := "" | |
| if len(fields) > 1 { | |
| unit = strings.Join(fields[1:], " ") | |
| } | |
| if strings.Contains(numeric, ".") { | |
| numeric = strings.TrimRight(strings.TrimRight(numeric, "0"), ".") | |
| if numeric == "" { | |
| numeric = "0" | |
| } | |
| } | |
| if unit != "" { | |
| br = numeric + " " + unit | |
| } else { | |
| br = numeric | |
| } |
This PR adds IMS call support for UE in SMF.
Note: - During testing with COTS UEs, IMS PDU session release behavior was observed and needs further analysis.