├── .github └── workflows │ ├── commit-msg-check.yml │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── cmd └── main.go ├── go.mod ├── go.sum ├── internal ├── context │ ├── bp_manager.go │ ├── charging.go │ ├── config.go │ ├── context.go │ ├── datapath.go │ ├── gsm_build.go │ ├── nf_profile.go │ ├── ngap_build.go │ ├── ngap_handler.go │ ├── pcc_rule.go │ ├── pco.go │ ├── pfcp_reports.go │ ├── pfcp_rules.go │ ├── pfcp_session_context.go │ ├── pool │ │ ├── lazyReusePool.go │ │ └── lazyReusePool_test.go │ ├── qos_flow.go │ ├── session_rules.go │ ├── sm_context.go │ ├── sm_context_policy.go │ ├── sm_context_policy_test.go │ ├── sm_ue.go │ ├── snssai.go │ ├── snssai_dnn_smf_info.go │ ├── timer.go │ ├── timer_test.go │ ├── traffic_control_data.go │ ├── ue_datapath.go │ ├── ue_datapath_test.go │ ├── ue_defaultPath.go │ ├── ue_ip_pool.go │ ├── ue_ip_pool_test.go │ ├── ulcl_group.go │ ├── upf.go │ ├── upf_test.go │ ├── user_plane_information.go │ └── user_plane_information_test.go ├── logger │ └── logger.go ├── pfcp │ ├── dispatcher.go │ ├── handler │ │ ├── handler.go │ │ └── handler_test.go │ ├── message │ │ ├── build.go │ │ ├── build_test.go │ │ ├── send.go │ │ └── send_test.go │ ├── reliable_pfcp_request_test.go │ └── udp │ │ ├── udp.go │ │ └── udp_test.go ├── sbi │ ├── api_callback.go │ ├── api_eventexposure.go │ ├── api_oam.go │ ├── api_pdusession.go │ ├── api_upi.go │ ├── consumer │ │ ├── amf_service.go │ │ ├── chf_service.go │ │ ├── consumer.go │ │ ├── nrf_service.go │ │ ├── pcf_service.go │ │ ├── pcf_service_test.go │ │ ├── smf_service.go │ │ └── udm_service.go │ ├── processor │ │ ├── association.go │ │ ├── charging_trigger.go │ │ ├── datapath.go │ │ ├── gsm_handler.go │ │ ├── gsm_handler_test.go │ │ ├── notifier.go │ │ ├── oam.go │ │ ├── pdu_session.go │ │ ├── pdu_session_test.go │ │ ├── processor.go │ │ ├── sm_common.go │ │ └── ulcl_procedure.go │ ├── routes.go │ └── server.go └── util │ ├── oauth │ ├── router_auth_check.go │ └── router_auth_check_test.go │ ├── qos_convert.go │ ├── qos_convert_test.go │ └── search_nf_service.go └── pkg ├── app ├── app.go └── mock.go ├── errors └── errors.go ├── factory ├── config.go ├── config_test.go └── factory.go ├── service ├── init.go └── mock.go └── utils └── pfcp_util.go /.github/workflows/commit-msg-check.yml: -------------------------------------------------------------------------------- 1 | name: 'Commit Message Check' 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | name: Conventional Commits 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: webiny/action-conventional-commits@v1.3.0 13 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | go: [ '1.21' ] 14 | name: Go ${{ matrix.go }} sample 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: [ main ] 8 | pull_request: 9 | 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: [ '1.21' ] 17 | steps: 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go }} 22 | - uses: actions/checkout@v4 23 | - name: Run golangci-lint 24 | uses: golangci/golangci-lint-action@v4 25 | with: 26 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 27 | version: v1.57.2 28 | 29 | # Optional: working directory, useful for monorepos 30 | # working-directory: somedir 31 | 32 | # Optional: golangci-lint command line arguments. 33 | # args: --issues-exit-code=0 34 | 35 | # Optional: show only new issues if it's a pull request. The default value is `false`. 36 | # only-new-issues: true 37 | 38 | # Optional: if set to true then the all caching functionality will be complete disabled, 39 | # takes precedence over all other caching options. 40 | # skip-cache: true 41 | 42 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 43 | # skip-pkg-cache: true 44 | 45 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 46 | # skip-build-cache: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Swap files 2 | *.swp 3 | 4 | # Toolchain 5 | # Golang project folder 6 | .idea/ 7 | 8 | # Visual Studio Code 9 | .vscode/ 10 | 11 | # Build 12 | build/ 13 | log/ 14 | vendor/ 15 | 16 | # emacs/vim 17 | GPATH 18 | GRTAGS 19 | GTAGS 20 | TAGS 21 | tags 22 | cscope.* 23 | 24 | # macOS 25 | .DS_Store 26 | 27 | # Debug 28 | *.log 29 | *.pcap -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "runtime/debug" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/urfave/cli" 14 | 15 | "github.com/free5gc/smf/internal/logger" 16 | "github.com/free5gc/smf/pkg/factory" 17 | "github.com/free5gc/smf/pkg/service" 18 | "github.com/free5gc/smf/pkg/utils" 19 | logger_util "github.com/free5gc/util/logger" 20 | "github.com/free5gc/util/version" 21 | ) 22 | 23 | var SMF *service.SmfApp 24 | 25 | func main() { 26 | defer func() { 27 | if p := recover(); p != nil { 28 | // Print stack for panic to log. Fatalf() will let program exit. 29 | logger.MainLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 30 | } 31 | }() 32 | 33 | app := cli.NewApp() 34 | app.Name = "smf" 35 | app.Usage = "5G Session Management Function (SMF)" 36 | app.Action = action 37 | app.Flags = []cli.Flag{ 38 | cli.StringFlag{ 39 | Name: "config, c", 40 | Usage: "Load configuration from `FILE`", 41 | }, 42 | cli.StringFlag{ 43 | Name: "uerouting, u", 44 | Usage: "Load uerouting configuration from `FILE`", 45 | }, 46 | cli.StringSliceFlag{ 47 | Name: "log, l", 48 | Usage: "Output NF log to `FILE`", 49 | }, 50 | } 51 | rand.New(rand.NewSource(time.Now().UnixNano())) 52 | 53 | if err := app.Run(os.Args); err != nil { 54 | logger.MainLog.Errorf("SMF Run error: %v\n", err) 55 | } 56 | } 57 | 58 | func action(cliCtx *cli.Context) error { 59 | tlsKeyLogPath, err := initLogFile(cliCtx.StringSlice("log")) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | logger.MainLog.Infoln("SMF version: ", version.GetVersion()) 65 | 66 | ctx, cancel := context.WithCancel(context.Background()) 67 | sigCh := make(chan os.Signal, 1) 68 | signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) 69 | 70 | go func() { 71 | <-sigCh // Wait for interrupt signal to gracefully shutdown 72 | cancel() // Notify each goroutine and wait them stopped 73 | }() 74 | 75 | cfg, err := factory.ReadConfig(cliCtx.String("config")) 76 | if err != nil { 77 | sigCh <- nil 78 | return err 79 | } 80 | factory.SmfConfig = cfg 81 | 82 | ueRoutingCfg, err := factory.ReadUERoutingConfig(cliCtx.String("uerouting")) 83 | if err != nil { 84 | sigCh <- nil 85 | return err 86 | } 87 | factory.UERoutingConfig = ueRoutingCfg 88 | 89 | pfcpStart, pfcpTerminate := utils.InitPFCPFunc(ctx) 90 | smf, err := service.NewApp(ctx, cfg, tlsKeyLogPath, pfcpStart, pfcpTerminate) 91 | if err != nil { 92 | sigCh <- nil 93 | return err 94 | } 95 | SMF = smf 96 | 97 | smf.Start() 98 | 99 | return nil 100 | } 101 | 102 | func initLogFile(logNfPath []string) (string, error) { 103 | logTlsKeyPath := "" 104 | 105 | for _, path := range logNfPath { 106 | if err := logger_util.LogFileHook(logger.Log, path); err != nil { 107 | return "", err 108 | } 109 | 110 | if logTlsKeyPath != "" { 111 | continue 112 | } 113 | 114 | nfDir, _ := filepath.Split(path) 115 | tmpDir := filepath.Join(nfDir, "key") 116 | if err := os.MkdirAll(tmpDir, 0o775); err != nil { 117 | logger.InitLog.Errorf("Make directory %s failed: %+v", tmpDir, err) 118 | return "", err 119 | } 120 | _, name := filepath.Split(factory.SmfDefaultTLSKeyLogPath) 121 | logTlsKeyPath = filepath.Join(tmpDir, name) 122 | } 123 | 124 | return logTlsKeyPath, nil 125 | } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/free5gc/smf 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/free5gc/aper v1.0.6-0.20250102035630-3ddc831eed6a 9 | github.com/free5gc/nas v1.1.5 10 | github.com/free5gc/ngap v1.0.10 11 | github.com/free5gc/openapi v1.1.0 12 | github.com/free5gc/pfcp v1.0.7 13 | github.com/free5gc/util v1.0.6 14 | github.com/gin-gonic/gin v1.9.1 15 | github.com/google/uuid v1.3.0 16 | github.com/h2non/gock v1.2.0 17 | github.com/pkg/errors v0.9.1 18 | github.com/sirupsen/logrus v1.9.3 19 | github.com/smartystreets/goconvey v1.8.1 20 | github.com/stretchr/testify v1.9.0 21 | github.com/urfave/cli v1.22.5 22 | go.uber.org/mock v0.4.0 23 | gopkg.in/yaml.v2 v2.4.0 24 | ) 25 | 26 | require ( 27 | github.com/bytedance/sonic v1.9.1 // indirect 28 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 29 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/free5gc/tlv v1.0.2 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-logr/logr v1.4.1 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/go-playground/validator/v10 v10.14.0 // indirect 39 | github.com/goccy/go-json v0.10.2 // indirect 40 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 41 | github.com/gopherjs/gopherjs v1.17.2 // indirect 42 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/jtolds/gls v4.20.0+incompatible // indirect 45 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 46 | github.com/kr/pretty v0.3.1 // indirect 47 | github.com/leodido/go-urn v1.2.4 // indirect 48 | github.com/mattn/go-isatty v0.0.19 // indirect 49 | github.com/mitchellh/mapstructure v1.5.0 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.2 // indirect 52 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 53 | github.com/pmezard/go-difflib v1.0.0 // indirect 54 | github.com/rogpeppe/go-internal v1.12.0 // indirect 55 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 56 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 57 | github.com/smarty/assertions v1.15.0 // indirect 58 | github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect 59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 60 | github.com/ugorji/go/codec v1.2.11 // indirect 61 | go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect 62 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 63 | go.opentelemetry.io/otel v1.24.0 // indirect 64 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 65 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 66 | golang.org/x/arch v0.3.0 // indirect 67 | golang.org/x/crypto v0.31.0 // indirect 68 | golang.org/x/net v0.33.0 // indirect 69 | golang.org/x/oauth2 v0.21.0 // indirect 70 | golang.org/x/sys v0.28.0 // indirect 71 | golang.org/x/text v0.21.0 // indirect 72 | google.golang.org/protobuf v1.33.0 // indirect 73 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /internal/context/bp_manager.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type BPManager struct { 8 | BPStatus BPStatus 9 | AddingPSAState AddingPSAState 10 | // Need these variable conducting Add additional PSA (TS23.502 4.3.5.4) 11 | // There value will change from time to time 12 | 13 | ActivatedPaths []*DataPath 14 | ActivatingPath *DataPath 15 | UpdatedBranchingPoint map[*UPF]int 16 | ULCL *UPF 17 | } 18 | type BPStatus int 19 | 20 | const ( 21 | UnInitialized BPStatus = iota 22 | AddingPSA 23 | AddPSASuccess 24 | InitializedSuccess 25 | InitializedFail 26 | ) 27 | 28 | type AddingPSAState int 29 | 30 | const ( 31 | ActivatingDataPath AddingPSAState = iota 32 | EstablishingNewPSA 33 | EstablishingULCL 34 | UpdatingPSA2DownLink 35 | UpdatingRANAndIUPFUpLink 36 | Finished 37 | ) 38 | 39 | func NewBPManager(supi string) (bpManager *BPManager) { 40 | bpManager = &BPManager{ 41 | BPStatus: UnInitialized, 42 | AddingPSAState: ActivatingDataPath, 43 | ActivatedPaths: make([]*DataPath, 0), 44 | UpdatedBranchingPoint: make(map[*UPF]int), 45 | } 46 | 47 | return 48 | } 49 | 50 | func (bpMGR *BPManager) SelectPSA2(smContext *SMContext) { 51 | hasSelectPSA2 := false 52 | bpMGR.ActivatedPaths = []*DataPath{} 53 | for _, dataPath := range smContext.Tunnel.DataPathPool { 54 | if dataPath.Activated { 55 | bpMGR.ActivatedPaths = append(bpMGR.ActivatedPaths, dataPath) 56 | } else if !hasSelectPSA2 { 57 | bpMGR.ActivatingPath = dataPath 58 | hasSelectPSA2 = true 59 | } 60 | } 61 | } 62 | 63 | func (bpMGR *BPManager) FindULCL(smContext *SMContext) error { 64 | bpMGR.UpdatedBranchingPoint = make(map[*UPF]int) 65 | activatingPath := bpMGR.ActivatingPath 66 | for _, psa1Path := range bpMGR.ActivatedPaths { 67 | depth := 0 68 | psa1CurDPNode := psa1Path.FirstDPNode 69 | for psa2CurDPNode := activatingPath.FirstDPNode; psa2CurDPNode != nil; psa2CurDPNode = psa2CurDPNode.Next() { 70 | if reflect.DeepEqual(psa2CurDPNode.UPF.NodeID, psa1CurDPNode.UPF.NodeID) { 71 | psa1CurDPNode = psa1CurDPNode.Next() 72 | depth++ 73 | 74 | if _, exist := bpMGR.UpdatedBranchingPoint[psa2CurDPNode.UPF]; !exist { 75 | bpMGR.UpdatedBranchingPoint[psa2CurDPNode.UPF] = depth 76 | } 77 | } else { 78 | break 79 | } 80 | } 81 | } 82 | 83 | maxDepth := 0 84 | for upf, depth := range bpMGR.UpdatedBranchingPoint { 85 | if depth > maxDepth { 86 | bpMGR.ULCL = upf 87 | maxDepth = depth 88 | } 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/context/charging.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/openapi/models" 5 | ) 6 | 7 | type ChargingLevel uint8 8 | 9 | // For a rating group that is pdu session charging level, all volume in a pdu session will be charged 10 | // For a rating group that is flow charging level (or Rating group level (32.255)), 11 | // only volume in a flow will be charged 12 | const ( 13 | PduSessionCharging ChargingLevel = iota 14 | FlowCharging 15 | ) 16 | 17 | type RequestType uint8 18 | 19 | // For each charging event, it will have a corresponding charging request type, see 32.255 Table 5.2.1.4.1 20 | const ( 21 | CHARGING_INIT RequestType = iota 22 | CHARGING_UPDATE 23 | CHARGING_RELEASE 24 | ) 25 | 26 | type ChargingInfo struct { 27 | ChargingMethod models.QuotaManagementIndicator 28 | VolumeLimitExpiryTimer *Timer 29 | EventLimitExpiryTimer *Timer 30 | ChargingLevel ChargingLevel 31 | RatingGroup int32 32 | UpfId string 33 | } 34 | -------------------------------------------------------------------------------- /internal/context/config.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/smf/pkg/factory" 5 | ) 6 | 7 | func SetupSMFContext(config *factory.Config) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/context/nf_profile.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/free5gc/openapi/models" 8 | "github.com/free5gc/smf/pkg/factory" 9 | ) 10 | 11 | type NFProfile struct { 12 | NFServices *[]models.NrfNfManagementNfService 13 | NFServiceVersion *[]models.NfServiceVersion 14 | SMFInfo *models.SmfInfo 15 | PLMNList *[]models.PlmnId 16 | } 17 | 18 | func (c *SMFContext) SetupNFProfile(nfProfileconfig *factory.Config) { 19 | // Set time 20 | nfSetupTime := time.Now() 21 | 22 | // set NfServiceVersion 23 | c.NfProfile.NFServiceVersion = &[]models.NfServiceVersion{ 24 | { 25 | ApiVersionInUri: "v1", 26 | ApiFullVersion: fmt. 27 | Sprintf("https://%s:%d"+factory.SmfPdusessionResUriPrefix, GetSelf().RegisterIPv4, GetSelf().SBIPort), 28 | Expiry: &nfSetupTime, 29 | }, 30 | } 31 | 32 | // set NFServices 33 | c.NfProfile.NFServices = new([]models.NrfNfManagementNfService) 34 | for _, serviceName := range nfProfileconfig.Configuration.ServiceNameList { 35 | *c.NfProfile.NFServices = append(*c.NfProfile.NFServices, models.NrfNfManagementNfService{ 36 | ServiceInstanceId: GetSelf().NfInstanceID + serviceName, 37 | ServiceName: models.ServiceName(serviceName), 38 | Versions: *c.NfProfile.NFServiceVersion, 39 | Scheme: models.UriScheme_HTTPS, 40 | NfServiceStatus: models.NfServiceStatus_REGISTERED, 41 | ApiPrefix: fmt.Sprintf("%s://%s:%d", GetSelf().URIScheme, GetSelf().RegisterIPv4, GetSelf().SBIPort), 42 | IpEndPoints: []models.IpEndPoint{ 43 | { 44 | Ipv4Address: GetSelf().RegisterIPv4, 45 | Port: int32(GetSelf().SBIPort), 46 | }, 47 | }, 48 | }) 49 | } 50 | 51 | // set smfInfo 52 | c.NfProfile.SMFInfo = &models.SmfInfo{ 53 | SNssaiSmfInfoList: SNssaiSmfInfo(), 54 | } 55 | 56 | // set PlmnList if exists 57 | if plmnList := nfProfileconfig.Configuration.PLMNList; plmnList != nil { 58 | c.NfProfile.PLMNList = new([]models.PlmnId) 59 | for _, plmn := range plmnList { 60 | *c.NfProfile.PLMNList = append(*c.NfProfile.PLMNList, models.PlmnId{ 61 | Mcc: plmn.Mcc, 62 | Mnc: plmn.Mnc, 63 | }) 64 | } 65 | } 66 | } 67 | 68 | func SNssaiSmfInfo() []models.SnssaiSmfInfoItem { 69 | snssaiInfo := make([]models.SnssaiSmfInfoItem, 0) 70 | for _, snssai := range smfContext.SnssaiInfos { 71 | var snssaiInfoModel models.SnssaiSmfInfoItem 72 | snssaiInfoModel.SNssai = &models.ExtSnssai{ 73 | Sst: snssai.Snssai.Sst, 74 | Sd: snssai.Snssai.Sd, 75 | } 76 | dnnModelList := make([]models.DnnSmfInfoItem, 0) 77 | 78 | for dnn := range snssai.DnnInfos { 79 | dnnModelList = append(dnnModelList, models.DnnSmfInfoItem{ 80 | Dnn: dnn, 81 | }) 82 | } 83 | 84 | snssaiInfoModel.DnnSmfInfoList = dnnModelList 85 | 86 | snssaiInfo = append(snssaiInfo, snssaiInfoModel) 87 | } 88 | return snssaiInfo 89 | } 90 | -------------------------------------------------------------------------------- /internal/context/pco.go: -------------------------------------------------------------------------------- 1 | // ProtocolConfigurationOptions 2 | package context 3 | 4 | type ProtocolConfigurationOptions struct { 5 | DNSIPv4Request bool 6 | DNSIPv6Request bool 7 | PCSCFIPv4Request bool 8 | IPv4LinkMTURequest bool 9 | } 10 | -------------------------------------------------------------------------------- /internal/context/pfcp_reports.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/openapi/models" 5 | "github.com/free5gc/pfcp" 6 | "github.com/free5gc/pfcp/pfcpType" 7 | "github.com/free5gc/smf/internal/logger" 8 | ) 9 | 10 | func (smContext *SMContext) HandleReports( 11 | usageReportRequest []*pfcp.UsageReportPFCPSessionReportRequest, 12 | usageReportModification []*pfcp.UsageReportPFCPSessionModificationResponse, 13 | usageReportDeletion []*pfcp.UsageReportPFCPSessionDeletionResponse, 14 | nodeId pfcpType.NodeID, reportTpye models.ChfConvergedChargingTriggerType, 15 | ) { 16 | var usageReport UsageReport 17 | upf := RetrieveUPFNodeByNodeID(nodeId) 18 | upfId := upf.UUID() 19 | 20 | for _, report := range usageReportRequest { 21 | usageReport.UrrId = report.URRID.UrrIdValue 22 | usageReport.UpfId = upfId 23 | usageReport.TotalVolume = report.VolumeMeasurement.TotalVolume 24 | usageReport.UplinkVolume = report.VolumeMeasurement.UplinkVolume 25 | usageReport.DownlinkVolume = report.VolumeMeasurement.DownlinkVolume 26 | usageReport.TotalPktNum = report.VolumeMeasurement.TotalPktNum 27 | usageReport.UplinkPktNum = report.VolumeMeasurement.UplinkPktNum 28 | usageReport.DownlinkPktNum = report.VolumeMeasurement.DownlinkPktNum 29 | usageReport.ReportTpye = identityTriggerType(report.UsageReportTrigger) 30 | 31 | if reportTpye != "" { 32 | usageReport.ReportTpye = reportTpye 33 | } 34 | 35 | smContext.UrrReports = append(smContext.UrrReports, usageReport) 36 | } 37 | for _, report := range usageReportModification { 38 | usageReport.UrrId = report.URRID.UrrIdValue 39 | usageReport.UpfId = upfId 40 | usageReport.TotalVolume = report.VolumeMeasurement.TotalVolume 41 | usageReport.UplinkVolume = report.VolumeMeasurement.UplinkVolume 42 | usageReport.DownlinkVolume = report.VolumeMeasurement.DownlinkVolume 43 | usageReport.TotalPktNum = report.VolumeMeasurement.TotalPktNum 44 | usageReport.UplinkPktNum = report.VolumeMeasurement.UplinkPktNum 45 | usageReport.DownlinkPktNum = report.VolumeMeasurement.DownlinkPktNum 46 | usageReport.ReportTpye = identityTriggerType(report.UsageReportTrigger) 47 | 48 | if reportTpye != "" { 49 | usageReport.ReportTpye = reportTpye 50 | } 51 | 52 | smContext.UrrReports = append(smContext.UrrReports, usageReport) 53 | } 54 | for _, report := range usageReportDeletion { 55 | usageReport.UrrId = report.URRID.UrrIdValue 56 | usageReport.UpfId = upfId 57 | usageReport.TotalVolume = report.VolumeMeasurement.TotalVolume 58 | usageReport.UplinkVolume = report.VolumeMeasurement.UplinkVolume 59 | usageReport.DownlinkVolume = report.VolumeMeasurement.DownlinkVolume 60 | usageReport.TotalPktNum = report.VolumeMeasurement.TotalPktNum 61 | usageReport.UplinkPktNum = report.VolumeMeasurement.UplinkPktNum 62 | usageReport.DownlinkPktNum = report.VolumeMeasurement.DownlinkPktNum 63 | usageReport.ReportTpye = identityTriggerType(report.UsageReportTrigger) 64 | 65 | if reportTpye != "" { 66 | usageReport.ReportTpye = reportTpye 67 | } 68 | 69 | smContext.UrrReports = append(smContext.UrrReports, usageReport) 70 | } 71 | } 72 | 73 | func identityTriggerType(usarTrigger *pfcpType.UsageReportTrigger) models.ChfConvergedChargingTriggerType { 74 | var trigger models.ChfConvergedChargingTriggerType 75 | 76 | switch { 77 | case usarTrigger.Volth: 78 | trigger = models.ChfConvergedChargingTriggerType_QUOTA_THRESHOLD 79 | case usarTrigger.Volqu: 80 | trigger = models.ChfConvergedChargingTriggerType_QUOTA_EXHAUSTED 81 | case usarTrigger.Quvti: 82 | trigger = models.ChfConvergedChargingTriggerType_VALIDITY_TIME 83 | case usarTrigger.Start: 84 | trigger = models.ChfConvergedChargingTriggerType_START_OF_SERVICE_DATA_FLOW 85 | case usarTrigger.Immer: 86 | logger.PduSessLog.Trace("Reports Query by SMF, trigger should be filled later") 87 | return "" 88 | case usarTrigger.Termr: 89 | trigger = models.ChfConvergedChargingTriggerType_FINAL 90 | default: 91 | logger.PduSessLog.Trace("Report is not a charging trigger") 92 | return "" 93 | } 94 | 95 | return trigger 96 | } 97 | -------------------------------------------------------------------------------- /internal/context/pfcp_rules.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/free5gc/pfcp/pfcpType" 7 | ) 8 | 9 | const ( 10 | RULE_INITIAL RuleState = 0 11 | RULE_CREATE RuleState = 1 12 | RULE_UPDATE RuleState = 2 13 | RULE_REMOVE RuleState = 3 14 | RULE_QUERY RuleState = 4 15 | ) 16 | 17 | type RuleState uint8 18 | 19 | // Packet Detection Rule. Table 7.5.2.2-1 20 | type PDR struct { 21 | PDRID uint16 22 | 23 | Precedence uint32 24 | PDI PDI 25 | OuterHeaderRemoval *pfcpType.OuterHeaderRemoval 26 | 27 | FAR *FAR 28 | URR []*URR 29 | QER []*QER 30 | 31 | State RuleState 32 | } 33 | 34 | const ( 35 | MeasureInfoMNOP = 0x10 // Measure Num of Pkts (MNOP) 36 | MeasureInfoMBQE = 0x1 // Measure Before Qos Enforce(MQBE) 37 | MesureMethodVol = "vol" 38 | MesureMethodTime = "time" 39 | MeasurePeriodReport = 0x0100 // 0x10: PERIO 40 | ) 41 | 42 | // Usage Report Rule 43 | type URR struct { 44 | URRID uint32 45 | MeasureMethod string // vol or time 46 | ReportingTrigger pfcpType.ReportingTriggers 47 | MeasurementPeriod time.Duration 48 | QuotaValidityTime time.Time 49 | MeasurementInformation pfcpType.MeasurementInformation 50 | VolumeThreshold uint64 51 | VolumeQuota uint64 52 | State RuleState 53 | } 54 | 55 | type UrrOpt func(urr *URR) 56 | 57 | func NewMeasureInformation(isMeasurePkt, isMeasureBeforeQos bool) UrrOpt { 58 | return func(urr *URR) { 59 | urr.MeasurementInformation.Mnop = isMeasurePkt 60 | urr.MeasurementInformation.Mbqe = isMeasureBeforeQos 61 | } 62 | } 63 | 64 | func NewMeasurementPeriod(time time.Duration) UrrOpt { 65 | return func(urr *URR) { 66 | urr.ReportingTrigger.Perio = true 67 | urr.MeasurementPeriod = time 68 | } 69 | } 70 | 71 | func NewVolumeThreshold(threshold uint64) UrrOpt { 72 | return func(urr *URR) { 73 | urr.ReportingTrigger.Volth = true 74 | urr.VolumeThreshold = threshold 75 | } 76 | } 77 | 78 | func NewVolumeQuota(quota uint64) UrrOpt { 79 | return func(urr *URR) { 80 | urr.ReportingTrigger.Volqu = true 81 | urr.VolumeQuota = quota 82 | } 83 | } 84 | 85 | func SetStartOfSDFTrigger() UrrOpt { 86 | return func(urr *URR) { 87 | urr.ReportingTrigger.Start = true 88 | } 89 | } 90 | 91 | func MeasureInformation(isMeasurePkt, isMeasureBeforeQos bool) pfcpType.MeasurementInformation { 92 | var measureInformation pfcpType.MeasurementInformation 93 | measureInformation.Mnop = isMeasurePkt 94 | measureInformation.Mbqe = isMeasureBeforeQos 95 | return measureInformation 96 | } 97 | 98 | func (pdr *PDR) AppendURRs(urrs []*URR) { 99 | for _, urr := range urrs { 100 | if !isUrrExist(pdr.URR, urr) { 101 | pdr.URR = append(pdr.URR, urr) 102 | } 103 | } 104 | } 105 | 106 | func isUrrExist(urrs []*URR, urr *URR) bool { // check if urr is in URRs list 107 | for _, URR := range urrs { 108 | if urr.URRID == URR.URRID { 109 | return true 110 | } 111 | } 112 | return false 113 | } 114 | 115 | // Packet Detection. 7.5.2.2-2 116 | type PDI struct { 117 | SourceInterface pfcpType.SourceInterface 118 | LocalFTeid *pfcpType.FTEID 119 | NetworkInstance *pfcpType.NetworkInstance 120 | UEIPAddress *pfcpType.UEIPAddress 121 | SDFFilter *pfcpType.SDFFilter 122 | ApplicationID string 123 | } 124 | 125 | // Forwarding Action Rule. 7.5.2.3-1 126 | type FAR struct { 127 | FARID uint32 128 | 129 | ApplyAction pfcpType.ApplyAction 130 | ForwardingParameters *ForwardingParameters 131 | 132 | BAR *BAR 133 | State RuleState 134 | } 135 | 136 | // Forwarding Parameters. 7.5.2.3-2 137 | type ForwardingParameters struct { 138 | DestinationInterface pfcpType.DestinationInterface 139 | NetworkInstance *pfcpType.NetworkInstance 140 | OuterHeaderCreation *pfcpType.OuterHeaderCreation 141 | ForwardingPolicyID string 142 | SendEndMarker bool 143 | } 144 | 145 | // Buffering Action Rule 7.5.2.6-1 146 | type BAR struct { 147 | BARID uint8 148 | 149 | DownlinkDataNotificationDelay pfcpType.DownlinkDataNotificationDelay 150 | SuggestedBufferingPacketsCount pfcpType.SuggestedBufferingPacketsCount 151 | 152 | State RuleState 153 | } 154 | 155 | // QoS Enhancement Rule 156 | type QER struct { 157 | QERID uint32 158 | 159 | QFI pfcpType.QFI 160 | 161 | GateStatus *pfcpType.GateStatus 162 | MBR *pfcpType.MBR 163 | GBR *pfcpType.GBR 164 | 165 | State RuleState 166 | } 167 | -------------------------------------------------------------------------------- /internal/context/pfcp_session_context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/free5gc/pfcp/pfcpType" 8 | ) 9 | 10 | type PFCPSessionResponseStatus int 11 | 12 | const ( 13 | SessionEstablishSuccess PFCPSessionResponseStatus = iota 14 | SessionEstablishFailed 15 | SessionUpdateSuccess 16 | SessionUpdateFailed 17 | SessionReleaseSuccess 18 | SessionReleaseFailed 19 | ) 20 | 21 | type FSEID struct { 22 | IP net.IP 23 | SEID uint64 24 | } 25 | 26 | type PFCPSessionContext struct { 27 | PDRs map[uint16]*PDR 28 | NodeID pfcpType.NodeID 29 | LocalSEID uint64 30 | RemoteSEID uint64 31 | } 32 | 33 | func (pfcpSessionContext *PFCPSessionContext) String() string { 34 | str := "\n" 35 | for pdrID, pdr := range pfcpSessionContext.PDRs { 36 | str += fmt.Sprintln("PDR ID: ", pdrID) 37 | str += fmt.Sprintf("PDR: %v\n", pdr) 38 | } 39 | 40 | str += fmt.Sprintln("Node ID: ", pfcpSessionContext.NodeID.ResolveNodeIdToIp().String()) 41 | str += fmt.Sprintln("LocalSEID: ", pfcpSessionContext.LocalSEID) 42 | str += fmt.Sprintln("RemoteSEID: ", pfcpSessionContext.RemoteSEID) 43 | str += "\n" 44 | 45 | return str 46 | } 47 | 48 | func (pfcpSessionResponseStatus PFCPSessionResponseStatus) String() string { 49 | switch pfcpSessionResponseStatus { 50 | case SessionUpdateSuccess: 51 | return "SessionUpdateSuccess" 52 | case SessionUpdateFailed: 53 | return "SessionUpdateFailed" 54 | case SessionReleaseSuccess: 55 | return "SessionReleaseSuccess" 56 | case SessionReleaseFailed: 57 | return "SessionReleaseFailed" 58 | default: 59 | return "Unknown PFCP Session Response Status" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/context/pool/lazyReusePool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type LazyReusePool struct { 9 | mtx sync.Mutex 10 | head *segment // nil when empty 11 | first int 12 | last int 13 | remain int 14 | } 15 | 16 | type segment struct { 17 | first int 18 | last int 19 | next *segment // nil when this segment is tail 20 | } 21 | 22 | type relativePos = int 23 | 24 | const ( 25 | withinThisSegment relativePos = iota 26 | adjacentToTheFront 27 | adjacentToTheBack 28 | before 29 | after 30 | ) 31 | 32 | // NewLazyReusePool makes a LazyReusePool. 33 | func NewLazyReusePool(first, last int) (*LazyReusePool, error) { 34 | if first > last { 35 | return nil, fmt.Errorf("make sure first(%d) <= last(%d)", first, last) 36 | } 37 | head := &segment{first, last, nil} 38 | return &LazyReusePool{ 39 | head: head, 40 | first: first, 41 | last: last, 42 | remain: last - first + 1, 43 | }, nil 44 | } 45 | 46 | func (p *LazyReusePool) Allocate() (res int, ok bool) { 47 | p.mtx.Lock() 48 | defer p.mtx.Unlock() 49 | 50 | if p.head == nil { 51 | return 0, false 52 | } 53 | res = p.head.first 54 | p.head.first++ 55 | if p.head.first > p.head.last { 56 | p.head = p.head.next 57 | } 58 | p.remain-- 59 | return res, true 60 | } 61 | 62 | func (p *LazyReusePool) Use(value int) bool { 63 | p.mtx.Lock() 64 | defer p.mtx.Unlock() 65 | 66 | if p.head == nil { 67 | return false 68 | } 69 | var prev *segment 70 | for cur := p.head; cur != nil; cur = cur.next { 71 | if cur.relativePosisionOf(value) == withinThisSegment { 72 | if cur.first == cur.last { 73 | if prev == nil { 74 | p.head = cur.next 75 | } else { 76 | prev.next = cur.next 77 | } 78 | } else { 79 | cur.split(value) 80 | } 81 | p.remain-- 82 | return true 83 | } 84 | prev = cur 85 | } 86 | return false 87 | } 88 | 89 | func (p *LazyReusePool) Free(value int) bool { 90 | p.mtx.Lock() 91 | defer p.mtx.Unlock() 92 | 93 | // Ensure the value is within this pool 94 | if value < p.first || p.last < value { 95 | return false 96 | } 97 | 98 | // When this pool is empty 99 | if p.head == nil { 100 | p.head = newSingleSegment(value) 101 | p.remain++ 102 | return true 103 | } 104 | 105 | // The value is returned into a segment after the head 106 | // for lazy reuse, excepted in the case of the value is 107 | // adjacent to the back of the head. 108 | 109 | switch p.head.relativePosisionOf(value) { 110 | case withinThisSegment: 111 | // duplecated free 112 | return false 113 | case adjacentToTheBack: 114 | // only in this case, returned into the head segment 115 | p.head.extendLast() 116 | p.remain++ 117 | return true 118 | } 119 | 120 | // When there is only head segment 121 | if p.head.next == nil { 122 | // add a new segment 123 | p.head.next = newSingleSegment(value) 124 | p.remain++ 125 | return true 126 | } 127 | 128 | prev := p.head 129 | cur := p.head.next 130 | for ; cur != nil; prev, cur = cur, cur.next { 131 | pos := cur.relativePosisionOf(value) 132 | switch pos { 133 | case before: 134 | // insert a segment 135 | temp := newSingleSegment(value) 136 | prev.next = temp 137 | temp.next = cur 138 | goto success 139 | case adjacentToTheFront: 140 | // extendFirst 141 | cur.first = value 142 | goto success 143 | case withinThisSegment: 144 | // duplecated free 145 | return false 146 | case adjacentToTheBack: 147 | cur.extendLast() 148 | goto success 149 | case after: 150 | if cur.next == nil { 151 | cur.next = newSingleSegment(value) 152 | goto success 153 | } 154 | // to next loop 155 | } 156 | } 157 | 158 | success: 159 | p.remain++ 160 | return true 161 | } 162 | 163 | func (p *LazyReusePool) Reserve(first, last int) error { 164 | if !p.Contains(first, last) { 165 | return fmt.Errorf("reserve range should in [%d, %d]", p.first, p.last) 166 | } 167 | 168 | for cur, prev := p.head, (*segment)(nil); cur != nil; cur = cur.next { 169 | switch { 170 | case cur.first >= first && cur.first <= last && cur.last > last: 171 | p.remain -= last - cur.first + 1 172 | cur.first = last + 1 173 | case cur.first < first && cur.last >= first && cur.last <= last: 174 | p.remain -= cur.last - first + 1 175 | cur.last = first - 1 176 | case cur.first < first && cur.last > last: 177 | cur.next = &segment{ 178 | first: last + 1, 179 | last: cur.last, 180 | next: cur.next, 181 | } 182 | 183 | cur.last = first - 1 184 | p.remain -= last - first + 1 185 | 186 | // this segment in reserve range 187 | case cur.first >= first && cur.last <= last: 188 | p.remain -= cur.last - cur.first + 1 189 | if prev != nil { 190 | prev.next = cur.next 191 | } else { 192 | p.head = cur.next 193 | } 194 | // do not update prev 195 | continue 196 | } 197 | 198 | prev = cur 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func (p *LazyReusePool) Contains(first, last int) bool { 205 | return first <= last && p.first <= first && p.last >= last 206 | } 207 | 208 | func (p *LazyReusePool) Min() int { 209 | return p.first 210 | } 211 | 212 | func (p *LazyReusePool) Max() int { 213 | return p.last 214 | } 215 | 216 | func (p *LazyReusePool) Remain() int { 217 | return p.remain 218 | } 219 | 220 | func (p *LazyReusePool) Total() int { 221 | return p.last - p.first + 1 222 | } 223 | 224 | func (p *LazyReusePool) GetHead() *segment { 225 | return p.head 226 | } 227 | 228 | func newSingleSegment(num int) *segment { 229 | return &segment{num, num, nil} 230 | } 231 | 232 | func (s *segment) relativePosisionOf(value int) relativePos { 233 | switch { 234 | case value < s.first-1: 235 | return before 236 | case value == s.first-1: 237 | return adjacentToTheFront 238 | case s.first <= value && value <= s.last: 239 | return withinThisSegment 240 | case value == s.last+1: 241 | return adjacentToTheBack 242 | default: 243 | return after 244 | } 245 | } 246 | 247 | func (s *segment) split(use int) bool { 248 | if use < s.first || use > s.last { 249 | return false 250 | } 251 | 252 | switch use { 253 | case s.first: 254 | s.first += 1 255 | case s.last: 256 | s.last -= 1 257 | default: 258 | next := &segment{ 259 | first: use + 1, 260 | last: s.last, 261 | next: s.next, 262 | } 263 | 264 | s.next = next 265 | s.last = use - 1 266 | } 267 | 268 | return true 269 | } 270 | 271 | func (s *segment) extendLast() *segment { 272 | s.last++ 273 | if s.next != nil && s.last+1 == s.next.first { 274 | // concatenate 275 | s.last = s.next.last 276 | s.next = s.next.next 277 | } 278 | return s 279 | } 280 | 281 | func (s *segment) First() int { 282 | return s.first 283 | } 284 | 285 | func (s *segment) Last() int { 286 | return s.last 287 | } 288 | 289 | func (s *segment) Next() *segment { 290 | return s.next 291 | } 292 | 293 | func (p1 *LazyReusePool) IsJoint(p2 *LazyReusePool) bool { 294 | if p2.last < p1.first || p1.last < p2.first { 295 | return false 296 | } 297 | return true 298 | } 299 | 300 | func (p *LazyReusePool) Dump() [][]int { 301 | var dumpedSegList [][]int 302 | curSeg := p.head 303 | for curSeg != nil { 304 | dumpedSeg := []int{curSeg.first, curSeg.last} 305 | dumpedSegList = append(dumpedSegList, dumpedSeg) 306 | curSeg = curSeg.next 307 | } 308 | return dumpedSegList 309 | } 310 | -------------------------------------------------------------------------------- /internal/context/qos_flow.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/aper" 5 | "github.com/free5gc/nas/nasType" 6 | "github.com/free5gc/ngap/ngapType" 7 | "github.com/free5gc/openapi/models" 8 | "github.com/free5gc/smf/internal/util" 9 | ) 10 | 11 | // QoSFlow - Policy and Charging Rule 12 | 13 | type QoSFlowState int 14 | 15 | const ( 16 | QoSFlowUnset QoSFlowState = iota 17 | QoSFlowSet 18 | QoSFlowToBeModify 19 | ) 20 | 21 | type QoSFlow struct { 22 | QFI uint8 23 | QoSProfile *models.QosData 24 | State QoSFlowState 25 | } 26 | 27 | func NewQoSFlow(qfi uint8, qosModel *models.QosData) *QoSFlow { 28 | if qosModel == nil { 29 | return nil 30 | } 31 | qos := &QoSFlow{ 32 | QFI: qfi, 33 | QoSProfile: qosModel, 34 | State: QoSFlowUnset, 35 | } 36 | return qos 37 | } 38 | 39 | func (q *QoSFlow) GetQFI() uint8 { 40 | return q.QFI 41 | } 42 | 43 | func (q *QoSFlow) Get5QI() uint8 { 44 | return uint8(q.QoSProfile.Var5qi) 45 | } 46 | 47 | func (q *QoSFlow) GetQoSProfile() *models.QosData { 48 | return q.QoSProfile 49 | } 50 | 51 | func (q *QoSFlow) IsGBRFlow() bool { 52 | return isGBRFlow(q.QoSProfile) 53 | } 54 | 55 | func (q *QoSFlow) BuildNasQoSDesc(opCode nasType.QoSFlowOperationCode) (nasType.QoSFlowDesc, error) { 56 | qosDesc := nasType.QoSFlowDesc{} 57 | qosDesc.QFI = q.GetQFI() 58 | qosDesc.OperationCode = opCode 59 | parameter := new(nasType.QoSFlow5QI) 60 | parameter.FiveQI = uint8(q.QoSProfile.Var5qi) 61 | qosDesc.Parameters = append(qosDesc.Parameters, parameter) 62 | 63 | if q.IsGBRFlow() && q.QoSProfile != nil { 64 | gbrDlParameter := new(nasType.QoSFlowGFBRDownlink) 65 | gbrDlParameter.Unit = nasType.QoSFlowBitRateUnit1Mbps 66 | gbrDlParameter.Value = util.BitRateTombps(q.QoSProfile.GbrDl) 67 | qosDesc.Parameters = append(qosDesc.Parameters, gbrDlParameter) 68 | gbrUlParameter := new(nasType.QoSFlowGFBRUplink) 69 | gbrUlParameter.Unit = nasType.QoSFlowBitRateUnit1Mbps 70 | gbrUlParameter.Value = util.BitRateTombps(q.QoSProfile.GbrUl) 71 | qosDesc.Parameters = append(qosDesc.Parameters, gbrUlParameter) 72 | mbrDlParameter := new(nasType.QoSFlowMFBRDownlink) 73 | mbrDlParameter.Unit = nasType.QoSFlowBitRateUnit1Mbps 74 | mbrDlParameter.Value = util.BitRateTombps(q.QoSProfile.MaxbrDl) 75 | qosDesc.Parameters = append(qosDesc.Parameters, mbrDlParameter) 76 | mbrUlParameter := new(nasType.QoSFlowMFBRUplink) 77 | mbrUlParameter.Unit = nasType.QoSFlowBitRateUnit1Mbps 78 | mbrUlParameter.Value = util.BitRateTombps(q.QoSProfile.MaxbrUl) 79 | qosDesc.Parameters = append(qosDesc.Parameters, mbrUlParameter) 80 | } 81 | return qosDesc, nil 82 | } 83 | 84 | func buildArpFromModels(arp *models.Arp) (int64, aper.Enumerated, aper.Enumerated) { 85 | if arp == nil { 86 | return 0, 0, 0 87 | } 88 | var arpPriorityLevel int64 89 | var arpPreEmptionCapability aper.Enumerated 90 | var arpPreEmptionVulnerability aper.Enumerated 91 | 92 | arpPriorityLevel = int64(arp.PriorityLevel) 93 | switch arp.PreemptCap { 94 | case models.PreemptionCapability_NOT_PREEMPT: 95 | arpPreEmptionCapability = ngapType.PreEmptionCapabilityPresentShallNotTriggerPreEmption 96 | case models.PreemptionCapability_MAY_PREEMPT: 97 | arpPreEmptionCapability = ngapType.PreEmptionCapabilityPresentMayTriggerPreEmption 98 | default: 99 | arpPreEmptionCapability = ngapType.PreEmptionCapabilityPresentShallNotTriggerPreEmption 100 | } 101 | switch arp.PreemptVuln { 102 | case models.PreemptionVulnerability_NOT_PREEMPTABLE: 103 | arpPreEmptionVulnerability = ngapType.PreEmptionVulnerabilityPresentNotPreEmptable 104 | case models.PreemptionVulnerability_PREEMPTABLE: 105 | arpPreEmptionVulnerability = ngapType.PreEmptionVulnerabilityPresentPreEmptable 106 | default: 107 | arpPreEmptionVulnerability = ngapType.PreEmptionVulnerabilityPresentNotPreEmptable 108 | } 109 | 110 | return arpPriorityLevel, arpPreEmptionCapability, arpPreEmptionVulnerability 111 | } 112 | 113 | func buildGBRQosInformationFromModel(qos *models.QosData) *ngapType.GBRQosInformation { 114 | if qos == nil { 115 | return nil 116 | } 117 | return &ngapType.GBRQosInformation{ 118 | MaximumFlowBitRateDL: util.StringToBitRate(qos.MaxbrDl), 119 | MaximumFlowBitRateUL: util.StringToBitRate(qos.MaxbrUl), 120 | GuaranteedFlowBitRateDL: util.StringToBitRate(qos.GbrDl), 121 | GuaranteedFlowBitRateUL: util.StringToBitRate(qos.GbrUl), 122 | } 123 | } 124 | 125 | func (q *QoSFlow) BuildNgapQosFlowSetupRequestItem() (ngapType.QosFlowSetupRequestItem, error) { 126 | qosDesc := ngapType.QosFlowSetupRequestItem{} 127 | 128 | qosDesc.QosFlowIdentifier = ngapType.QosFlowIdentifier{ 129 | Value: int64(q.GetQFI()), 130 | } 131 | 132 | parameter := ngapType.QosFlowLevelQosParameters{} 133 | parameter.QosCharacteristics = ngapType.QosCharacteristics{ 134 | Present: ngapType.QosCharacteristicsPresentNonDynamic5QI, 135 | NonDynamic5QI: &ngapType.NonDynamic5QIDescriptor{ 136 | FiveQI: ngapType.FiveQI{ 137 | Value: int64(q.Get5QI()), 138 | }, 139 | }, 140 | } 141 | 142 | if q.IsGBRFlow() { 143 | parameter.GBRQosInformation = buildGBRQosInformationFromModel(q.QoSProfile) 144 | } 145 | 146 | var arpPriorityLevel int64 147 | var arpPreEmptionCapability aper.Enumerated 148 | var arpPreEmptionVulnerability aper.Enumerated 149 | if arp := q.QoSProfile.Arp; arp != nil { 150 | arpPriorityLevel, 151 | arpPreEmptionCapability, 152 | arpPreEmptionVulnerability = buildArpFromModels(arp) 153 | } else { 154 | // TODO: should get value from PCF 155 | arpPriorityLevel = 8 156 | arpPreEmptionCapability = ngapType.PreEmptionCapabilityPresentShallNotTriggerPreEmption 157 | arpPreEmptionVulnerability = ngapType.PreEmptionVulnerabilityPresentNotPreEmptable 158 | } 159 | 160 | parameter.AllocationAndRetentionPriority = ngapType.AllocationAndRetentionPriority{ 161 | PriorityLevelARP: ngapType.PriorityLevelARP{ 162 | Value: arpPriorityLevel, 163 | }, 164 | PreEmptionCapability: ngapType.PreEmptionCapability{ 165 | Value: arpPreEmptionCapability, 166 | }, 167 | PreEmptionVulnerability: ngapType.PreEmptionVulnerability{ 168 | Value: arpPreEmptionVulnerability, 169 | }, 170 | } 171 | 172 | qosDesc.QosFlowLevelQosParameters = parameter 173 | 174 | return qosDesc, nil 175 | } 176 | 177 | func (q *QoSFlow) BuildNgapQosFlowAddOrModifyRequestItem() (ngapType.QosFlowAddOrModifyRequestItem, error) { 178 | qosDesc := ngapType.QosFlowAddOrModifyRequestItem{} 179 | 180 | qosDesc.QosFlowIdentifier = ngapType.QosFlowIdentifier{ 181 | Value: int64(q.GetQFI()), 182 | } 183 | 184 | parameter := ngapType.QosFlowLevelQosParameters{} 185 | parameter.QosCharacteristics = ngapType.QosCharacteristics{ 186 | Present: ngapType.QosCharacteristicsPresentNonDynamic5QI, 187 | NonDynamic5QI: &ngapType.NonDynamic5QIDescriptor{ 188 | FiveQI: ngapType.FiveQI{ 189 | Value: int64(q.Get5QI()), 190 | }, 191 | }, 192 | } 193 | 194 | if q.IsGBRFlow() { 195 | parameter.GBRQosInformation = buildGBRQosInformationFromModel(q.QoSProfile) 196 | } 197 | 198 | var arpPriorityLevel int64 199 | var arpPreEmptionCapability aper.Enumerated 200 | var arpPreEmptionVulnerability aper.Enumerated 201 | if arp := q.QoSProfile.Arp; arp != nil { 202 | arpPriorityLevel, 203 | arpPreEmptionCapability, 204 | arpPreEmptionVulnerability = buildArpFromModels(arp) 205 | } else { 206 | // TODO: should get value from PCF 207 | arpPriorityLevel = 8 208 | arpPreEmptionCapability = ngapType.PreEmptionCapabilityPresentShallNotTriggerPreEmption 209 | arpPreEmptionVulnerability = ngapType.PreEmptionVulnerabilityPresentNotPreEmptable 210 | } 211 | 212 | parameter.AllocationAndRetentionPriority = ngapType.AllocationAndRetentionPriority{ 213 | PriorityLevelARP: ngapType.PriorityLevelARP{ 214 | Value: arpPriorityLevel, 215 | }, 216 | PreEmptionCapability: ngapType.PreEmptionCapability{ 217 | Value: arpPreEmptionCapability, 218 | }, 219 | PreEmptionVulnerability: ngapType.PreEmptionVulnerability{ 220 | Value: arpPreEmptionVulnerability, 221 | }, 222 | } 223 | 224 | qosDesc.QosFlowLevelQosParameters = ¶meter 225 | 226 | return qosDesc, nil 227 | } 228 | -------------------------------------------------------------------------------- /internal/context/session_rules.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/openapi/models" 5 | ) 6 | 7 | // SessionRule - A session rule consists of policy information elements 8 | // associated with PDU session. 9 | type SessionRule struct { 10 | *models.SessionRule 11 | DefQosQFI uint8 12 | } 13 | 14 | // NewSessionRule - create session rule from OpenAPI models 15 | func NewSessionRule(model *models.SessionRule) *SessionRule { 16 | if model == nil { 17 | return nil 18 | } 19 | 20 | return &SessionRule{ 21 | SessionRule: model, 22 | DefQosQFI: 1, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/context/sm_ue.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "sync" 4 | 5 | type UeData struct { 6 | PduSessionCount int // store number of PDU Sessions for each UE 7 | SdmSubscriptionId string // store SDM Subscription ID per UE 8 | } 9 | 10 | type Ues struct { 11 | ues map[string]UeData // map to store UE data with SUPI as key 12 | mu sync.Mutex // mutex for concurrent access 13 | } 14 | 15 | func InitSmfUeData() *Ues { 16 | return &Ues{ 17 | ues: make(map[string]UeData), 18 | } 19 | } 20 | 21 | // IncrementPduSessionCount increments the PDU session count for a given UE. 22 | func (u *Ues) IncrementPduSessionCount(ueId string) { 23 | u.mu.Lock() 24 | defer u.mu.Unlock() 25 | 26 | ueData := u.ues[ueId] 27 | ueData.PduSessionCount++ 28 | u.ues[ueId] = ueData 29 | } 30 | 31 | // DecrementPduSessionCount decrements the PDU session count for a given UE. 32 | func (u *Ues) DecrementPduSessionCount(ueId string) { 33 | u.mu.Lock() 34 | defer u.mu.Unlock() 35 | 36 | ueData := u.ues[ueId] 37 | if ueData.PduSessionCount > 0 { 38 | ueData.PduSessionCount-- 39 | u.ues[ueId] = ueData 40 | } 41 | } 42 | 43 | // SetSubscriptionId sets the SDM subscription ID for a given UE. 44 | func (u *Ues) SetSubscriptionId(ueId, subscriptionId string) { 45 | u.mu.Lock() 46 | defer u.mu.Unlock() 47 | 48 | ueData := u.ues[ueId] 49 | ueData.SdmSubscriptionId = subscriptionId 50 | u.ues[ueId] = ueData 51 | } 52 | 53 | // GetSubscriptionId returns the SDM subscription ID for a given UE. 54 | func (u *Ues) GetSubscriptionId(ueId string) string { 55 | u.mu.Lock() 56 | defer u.mu.Unlock() 57 | 58 | return u.ues[ueId].SdmSubscriptionId 59 | } 60 | 61 | // GetUeData returns the data for a given UE. 62 | func (u *Ues) GetUeData(ueId string) UeData { 63 | u.mu.Lock() 64 | defer u.mu.Unlock() 65 | 66 | return u.ues[ueId] 67 | } 68 | 69 | // DeleteUe deletes a UE. 70 | func (u *Ues) DeleteUe(ueId string) { 71 | u.mu.Lock() 72 | defer u.mu.Unlock() 73 | 74 | delete(u.ues, ueId) 75 | } 76 | 77 | // UeExists checks if a UE already exists. 78 | func (u *Ues) UeExists(ueId string) bool { 79 | u.mu.Lock() 80 | defer u.mu.Unlock() 81 | 82 | _, exists := u.ues[ueId] 83 | return exists 84 | } 85 | 86 | // IsLastPduSession checks if it is the last PDU session for a given UE. 87 | func (u *Ues) IsLastPduSession(ueID string) bool { 88 | u.mu.Lock() 89 | defer u.mu.Unlock() 90 | 91 | return u.ues[ueID].PduSessionCount == 1 92 | } 93 | 94 | // GetPduSessionCount returns the number of sessions for a given UE. 95 | func (u *Ues) GetPduSessionCount(ueId string) int { 96 | u.mu.Lock() 97 | defer u.mu.Unlock() 98 | 99 | return u.ues[ueId].PduSessionCount 100 | } 101 | -------------------------------------------------------------------------------- /internal/context/snssai.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/free5gc/openapi/models" 8 | ) 9 | 10 | type SNssai struct { 11 | Sst int32 12 | Sd string 13 | } 14 | 15 | // Equal return true if two S-NSSAI is equal 16 | func (s *SNssai) Equal(target *SNssai) bool { 17 | return s.Sst == target.Sst && strings.EqualFold(s.Sd, target.Sd) 18 | } 19 | 20 | func (s *SNssai) EqualModelsSnssai(target *models.Snssai) bool { 21 | return s.Sst == target.Sst && strings.EqualFold(s.Sd, target.Sd) 22 | } 23 | 24 | type SnssaiUPFInfo struct { 25 | SNssai *SNssai 26 | DnnList []*DnnUPFInfoItem 27 | } 28 | 29 | // DnnUpfInfoItem presents UPF dnn information 30 | type DnnUPFInfoItem struct { 31 | Dnn string 32 | DnaiList []string 33 | PduSessionTypes []models.PduSessionType 34 | UeIPPools []*UeIPPool 35 | StaticIPPools []*UeIPPool 36 | } 37 | 38 | // ContainsDNAI return true if the this dnn Info contains the specify DNAI 39 | func (d *DnnUPFInfoItem) ContainsDNAI(targetDnai string) bool { 40 | if targetDnai == "" { 41 | return d.DnaiList == nil || len(d.DnaiList) == 0 42 | } 43 | for _, dnai := range d.DnaiList { 44 | if dnai == targetDnai { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | // ContainsIPPool returns true if the ip pool of this upf dnn info contains the `ip` 52 | func (d *DnnUPFInfoItem) ContainsIPPool(ip net.IP) bool { 53 | if ip == nil { 54 | return true 55 | } 56 | for _, ipPool := range d.UeIPPools { 57 | if ipPool.ueSubNet.Contains(ip) { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /internal/context/snssai_dnn_smf_info.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "net" 4 | 5 | // SnssaiSmfInfo records the SMF S-NSSAI related information 6 | type SnssaiSmfInfo struct { 7 | Snssai SNssai 8 | DnnInfos map[string]*SnssaiSmfDnnInfo 9 | } 10 | 11 | // SnssaiSmfDnnInfo records the SMF per S-NSSAI DNN information 12 | type SnssaiSmfDnnInfo struct { 13 | DNS DNS 14 | PCSCF PCSCF 15 | } 16 | 17 | type DNS struct { 18 | IPv4Addr net.IP 19 | IPv6Addr net.IP 20 | } 21 | 22 | type PCSCF struct { 23 | IPv4Addr net.IP 24 | } 25 | -------------------------------------------------------------------------------- /internal/context/timer.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "runtime/debug" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/free5gc/smf/internal/logger" 9 | ) 10 | 11 | // Timer can be used for retransmission, it will manage retry times automatically 12 | type Timer struct { 13 | ticker *time.Ticker 14 | expireTimes int32 // accessed atomically 15 | maxRetryTimes int32 // accessed atomically 16 | done chan bool 17 | } 18 | 19 | // NewTimer will return a Timer struct and create a goroutine. Then it calls expiredFunc every time interval d until 20 | // the user call Stop(). the number of expire event is be recorded when the timer is active. When the number of expire 21 | // event is > maxRetryTimes, then the timer will call cancelFunc and turns off itself. Whether expiredFunc pass a 22 | // parameter expireTimes to tell the user that the current expireTimes. 23 | func NewTimer(d time.Duration, maxRetryTimes int, 24 | expiredFunc func(expireTimes int32), 25 | cancelFunc func(), 26 | ) *Timer { 27 | t := &Timer{} 28 | atomic.StoreInt32(&t.expireTimes, 0) 29 | atomic.StoreInt32(&t.maxRetryTimes, int32(maxRetryTimes)) 30 | t.done = make(chan bool, 1) 31 | t.ticker = time.NewTicker(d) 32 | 33 | go func(ticker *time.Ticker) { 34 | defer func() { 35 | if p := recover(); p != nil { 36 | // Print stack for panic to log. Fatalf() will let program exit. 37 | logger.CtxLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 38 | } 39 | }() 40 | 41 | defer ticker.Stop() 42 | 43 | for { 44 | select { 45 | case <-t.done: 46 | return 47 | case <-ticker.C: 48 | atomic.AddInt32(&t.expireTimes, 1) 49 | if t.ExpireTimes() > t.MaxRetryTimes() { 50 | cancelFunc() 51 | return 52 | } else { 53 | expiredFunc(t.ExpireTimes()) 54 | } 55 | } 56 | } 57 | }(t.ticker) 58 | 59 | return t 60 | } 61 | 62 | // MaxRetryTimes return the max retry times of the timer 63 | func (t *Timer) MaxRetryTimes() int32 { 64 | return atomic.LoadInt32(&t.maxRetryTimes) 65 | } 66 | 67 | // ExpireTimes return the current expire times of the timer 68 | func (t *Timer) ExpireTimes() int32 { 69 | return atomic.LoadInt32(&t.expireTimes) 70 | } 71 | 72 | // Stop turns off the timer, after Stop, no more timeout event will be triggered. User should call Stop() only once 73 | // otherwise it may hang on writing to done channel 74 | func (t *Timer) Stop() { 75 | t.done <- true 76 | close(t.done) 77 | } 78 | -------------------------------------------------------------------------------- /internal/context/timer_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/free5gc/smf/internal/context" 10 | ) 11 | 12 | func TestTimerNewTimer(t *testing.T) { 13 | timer := context.NewTimer(100*time.Millisecond, 3, func(expireTimes int32) { 14 | t.Logf("expire %d times", expireTimes) 15 | }, func() { 16 | t.Log("exceed max retry times (3)") 17 | }) 18 | assert.NotNil(t, timer) 19 | } 20 | 21 | func TestTimerStartAndStop(t *testing.T) { 22 | timer := context.NewTimer(100*time.Millisecond, 3, 23 | func(expireTimes int32) { 24 | t.Logf("expire %d times", expireTimes) 25 | }, 26 | func() { 27 | t.Log("exceed max retry times (3)") 28 | }) 29 | assert.NotNil(t, timer) 30 | 31 | time.Sleep(350 * time.Millisecond) 32 | timer.Stop() 33 | assert.EqualValues(t, 3, timer.ExpireTimes()) 34 | } 35 | 36 | func TestTimerExceedMaxRetryTimes(t *testing.T) { 37 | timer := context.NewTimer(100*time.Millisecond, 3, 38 | func(expireTimes int32) { 39 | t.Logf("expire %d times", expireTimes) 40 | }, 41 | func() { 42 | t.Log("exceed max retry times (3)") 43 | }) 44 | assert.NotNil(t, timer) 45 | 46 | time.Sleep(450 * time.Millisecond) 47 | } 48 | -------------------------------------------------------------------------------- /internal/context/traffic_control_data.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/free5gc/openapi/models" 5 | ) 6 | 7 | // TrafficControlData - Traffic control data defines how traffic data flows 8 | // associated with a rule are treated (e.g. blocked, redirected). 9 | type TrafficControlData struct { 10 | *models.TrafficControlData 11 | } 12 | 13 | // NewTrafficControlData - create the traffic control data from OpenAPI model 14 | func NewTrafficControlData(model *models.TrafficControlData) *TrafficControlData { 15 | if model == nil { 16 | return nil 17 | } 18 | 19 | return &TrafficControlData{ 20 | TrafficControlData: model, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/context/ue_datapath.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/free5gc/smf/internal/logger" 8 | "github.com/free5gc/smf/pkg/factory" 9 | "github.com/free5gc/util/idgenerator" 10 | ) 11 | 12 | type UEPreConfigPaths struct { 13 | DataPathPool DataPathPool 14 | PathIDGenerator *idgenerator.IDGenerator 15 | } 16 | 17 | func NewUEDataPathNode(name string) (node *DataPathNode, err error) { 18 | upNodes := smfContext.UserPlaneInformation.UPNodes 19 | 20 | if _, exist := upNodes[name]; !exist { 21 | err = fmt.Errorf("UPNode %s isn't exist in smfcfg.yaml, but in UERouting.yaml!", name) 22 | return nil, err 23 | } 24 | 25 | node = &DataPathNode{ 26 | UPF: upNodes[name].UPF, 27 | UpLinkTunnel: >PTunnel{}, 28 | DownLinkTunnel: >PTunnel{}, 29 | } 30 | return 31 | } 32 | 33 | func NewUEPreConfigPaths(paths []factory.SpecificPath) (*UEPreConfigPaths, error) { 34 | var uePreConfigPaths *UEPreConfigPaths 35 | ueDataPathPool := NewDataPathPool() 36 | lowerBound := 0 37 | pathIDGenerator := idgenerator.NewGenerator(1, math.MaxInt32) 38 | 39 | logger.PduSessLog.Infoln("In NewUEPreConfigPaths") 40 | 41 | for _, path := range paths { 42 | dataPath := NewDataPath() 43 | 44 | var pathID int64 45 | if allocPathID, err := pathIDGenerator.Allocate(); err != nil { 46 | logger.CtxLog.Warnf("Allocate pathID error: %+v", err) 47 | return nil, err 48 | } else { 49 | pathID = allocPathID 50 | } 51 | 52 | dataPath.Destination.DestinationIP = path.DestinationIP 53 | dataPath.Destination.DestinationPort = path.DestinationPort 54 | ueDataPathPool[pathID] = dataPath 55 | var parentNode *DataPathNode = nil 56 | for idx, nodeName := range path.Path { 57 | newUeNode, err := NewUEDataPathNode(nodeName) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if idx == lowerBound { 63 | dataPath.FirstDPNode = newUeNode 64 | } 65 | if parentNode != nil { 66 | newUeNode.AddPrev(parentNode) 67 | parentNode.AddNext(newUeNode) 68 | } 69 | parentNode = newUeNode 70 | } 71 | 72 | logger.CtxLog.Traceln("New data path added") 73 | logger.CtxLog.Traceln("\n" + dataPath.String() + "\n") 74 | } 75 | 76 | uePreConfigPaths = &UEPreConfigPaths{ 77 | DataPathPool: ueDataPathPool, 78 | PathIDGenerator: pathIDGenerator, 79 | } 80 | return uePreConfigPaths, nil 81 | } 82 | 83 | func GetUEPreConfigPaths(supi string, upfName string) *UEPreConfigPaths { 84 | groupName := GetULCLGroupNameFromSUPI(supi) 85 | if groupName == "" { 86 | return nil 87 | } 88 | dataPathPool := NewDataPathPool() 89 | dataPathPool[1] = smfContext.UEDefaultPathPool[groupName].GetDefaultPath(upfName) 90 | var i int64 = 2 91 | for _, dataPath := range smfContext.UEPreConfigPathPool[groupName].DataPathPool { 92 | firstNode := dataPath.CopyFirstDPNode() 93 | path := &DataPath{ 94 | Activated: false, 95 | IsDefaultPath: false, 96 | Destination: dataPath.Destination, 97 | FirstDPNode: firstNode, 98 | } 99 | dataPathPool[i] = path 100 | i++ 101 | } 102 | paths := &UEPreConfigPaths{ 103 | DataPathPool: dataPathPool, 104 | PathIDGenerator: smfContext.UEPreConfigPathPool[groupName].PathIDGenerator, 105 | } 106 | return paths 107 | } 108 | 109 | func CheckUEHasPreConfig(supi string) (exist bool) { 110 | groupName := GetULCLGroupNameFromSUPI(supi) 111 | logger.CtxLog.Tracef("UE [%s] belongs to group [%s]", supi, groupName) 112 | if groupName == "" { 113 | return false 114 | } 115 | _, exist = smfContext.UEPreConfigPathPool[groupName] 116 | return 117 | } 118 | -------------------------------------------------------------------------------- /internal/context/ue_datapath_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/free5gc/smf/internal/context" 10 | "github.com/free5gc/smf/pkg/factory" 11 | ) 12 | 13 | var config = configuration 14 | 15 | // smfContext.UserPlaneInformation = NewUserPlaneInformation(config) 16 | 17 | func TestNewUEPreConfigPaths(t *testing.T) { 18 | smfContext := context.GetSelf() 19 | smfContext.UserPlaneInformation = context.NewUserPlaneInformation(config) 20 | fmt.Println("Start") 21 | testcases := []struct { 22 | name string 23 | inPaths []factory.SpecificPath 24 | expectedDataPathNodes [][]*context.UPF 25 | }{ 26 | { 27 | name: "singlePath-singleUPF", 28 | inPaths: []factory.SpecificPath{ 29 | { 30 | DestinationIP: "10.60.0.101/32", 31 | DestinationPort: "12345", 32 | Path: []string{ 33 | "UPF1", 34 | }, 35 | }, 36 | }, 37 | expectedDataPathNodes: [][]*context.UPF{ 38 | { 39 | getUpf("UPF1"), 40 | }, 41 | }, 42 | }, 43 | { 44 | name: "singlePath-multiUPF", 45 | inPaths: []factory.SpecificPath{ 46 | { 47 | DestinationIP: "10.60.0.101/32", 48 | DestinationPort: "12345", 49 | Path: []string{ 50 | "UPF1", 51 | "UPF2", 52 | }, 53 | }, 54 | }, 55 | expectedDataPathNodes: [][]*context.UPF{ 56 | { 57 | getUpf("UPF1"), 58 | getUpf("UPF2"), 59 | }, 60 | }, 61 | }, 62 | { 63 | name: "multiPath-singleUPF", 64 | inPaths: []factory.SpecificPath{ 65 | { 66 | DestinationIP: "10.60.0.101/32", 67 | DestinationPort: "12345", 68 | Path: []string{ 69 | "UPF1", 70 | }, 71 | }, 72 | { 73 | DestinationIP: "10.60.0.103/32", 74 | DestinationPort: "12345", 75 | Path: []string{ 76 | "UPF2", 77 | }, 78 | }, 79 | }, 80 | expectedDataPathNodes: [][]*context.UPF{ 81 | { 82 | getUpf("UPF1"), 83 | }, 84 | { 85 | getUpf("UPF2"), 86 | }, 87 | }, 88 | }, 89 | { 90 | name: "multiPath-multiUPF", 91 | inPaths: []factory.SpecificPath{ 92 | { 93 | DestinationIP: "10.60.0.101/32", 94 | DestinationPort: "12345", 95 | Path: []string{ 96 | "UPF1", 97 | "UPF2", 98 | }, 99 | }, 100 | { 101 | DestinationIP: "10.60.0.103/32", 102 | DestinationPort: "12345", 103 | Path: []string{ 104 | "UPF1", 105 | "UPF3", 106 | }, 107 | }, 108 | }, 109 | expectedDataPathNodes: [][]*context.UPF{ 110 | { 111 | getUpf("UPF1"), 112 | getUpf("UPF2"), 113 | }, 114 | { 115 | getUpf("UPF1"), 116 | getUpf("UPF3"), 117 | }, 118 | }, 119 | }, 120 | { 121 | name: "multiPath-single&multiUPF", 122 | inPaths: []factory.SpecificPath{ 123 | { 124 | DestinationIP: "10.60.0.101/32", 125 | DestinationPort: "12345", 126 | Path: []string{ 127 | "UPF1", 128 | }, 129 | }, 130 | { 131 | DestinationIP: "10.60.0.103/32", 132 | DestinationPort: "12345", 133 | Path: []string{ 134 | "UPF1", 135 | "UPF3", 136 | }, 137 | }, 138 | }, 139 | expectedDataPathNodes: [][]*context.UPF{ 140 | { 141 | getUpf("UPF1"), 142 | }, 143 | { 144 | getUpf("UPF1"), 145 | getUpf("UPF3"), 146 | }, 147 | }, 148 | }, 149 | } 150 | 151 | for _, tc := range testcases { 152 | t.Run(tc.name, func(t *testing.T) { 153 | retUePreConfigPaths, err := context.NewUEPreConfigPaths(tc.inPaths) 154 | require.Nil(t, err) 155 | require.NotNil(t, retUePreConfigPaths.PathIDGenerator) 156 | for pathIndex, path := range tc.inPaths { 157 | retDataPath := retUePreConfigPaths.DataPathPool[int64(pathIndex+1)] 158 | require.Equal(t, path.DestinationIP, retDataPath.Destination.DestinationIP) 159 | require.Equal(t, path.DestinationPort, retDataPath.Destination.DestinationPort) 160 | retNode := retDataPath.FirstDPNode 161 | for _, expectedUpf := range tc.expectedDataPathNodes[pathIndex] { 162 | require.NotNil(t, retNode.UPF) 163 | require.Equal(t, retNode.UPF, expectedUpf) 164 | retNode = retNode.DownLinkTunnel.SrcEndPoint 165 | } 166 | require.Nil(t, retNode) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | func getUpf(name string) *context.UPF { 173 | newUeNode, err := context.NewUEDataPathNode(name) 174 | if err != nil { 175 | return nil 176 | } 177 | 178 | Upf := newUeNode.UPF 179 | 180 | return Upf 181 | } 182 | -------------------------------------------------------------------------------- /internal/context/ue_defaultPath.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "sort" 9 | 10 | "github.com/free5gc/smf/internal/logger" 11 | "github.com/free5gc/smf/pkg/factory" 12 | ) 13 | 14 | type UEDefaultPaths struct { 15 | AnchorUPFs []string // list of UPF name 16 | DefaultPathPool DefaultPathPool 17 | } 18 | 19 | type DefaultPathPool map[string]*DataPath // key: UPF name 20 | 21 | func NewUEDefaultPaths(upi *UserPlaneInformation, topology []factory.UPLink) (*UEDefaultPaths, error) { 22 | logger.MainLog.Traceln("In NewUEDefaultPaths") 23 | 24 | defaultPathPool := make(map[string]*DataPath) 25 | source, err := findSourceInTopology(upi, topology) 26 | if err != nil { 27 | return nil, err 28 | } 29 | destinations, err := extractAnchorUPFForULCL(upi, source, topology) 30 | if err != nil { 31 | return nil, err 32 | } 33 | for _, destination := range destinations { 34 | path, errgenerate := generateDefaultDataPath(source, destination, topology) 35 | if errgenerate != nil { 36 | return nil, errgenerate 37 | } 38 | defaultPathPool[destination] = path 39 | } 40 | defautlPaths := &UEDefaultPaths{ 41 | AnchorUPFs: destinations, 42 | DefaultPathPool: defaultPathPool, 43 | } 44 | return defautlPaths, nil 45 | } 46 | 47 | func findSourceInTopology(upi *UserPlaneInformation, topology []factory.UPLink) (string, error) { 48 | sourceList := make([]string, 0) 49 | for key, node := range upi.AccessNetwork { 50 | if node.Type == UPNODE_AN { 51 | sourceList = append(sourceList, key) 52 | } 53 | } 54 | for _, anName := range sourceList { 55 | for _, link := range topology { 56 | if link.A == anName || link.B == anName { 57 | // if multiple gNBs exist, select one according to some criterion 58 | logger.InitLog.Debugf("%s is AN", anName) 59 | return anName, nil 60 | } 61 | } 62 | } 63 | return "", errors.New("Not found AN node in topology") 64 | } 65 | 66 | func extractAnchorUPFForULCL(upi *UserPlaneInformation, source string, topology []factory.UPLink) ([]string, error) { 67 | upList := make([]string, 0) 68 | visited := make(map[string]bool) 69 | queue := make([]string, 0) 70 | 71 | queue = append(queue, source) 72 | queued := make(map[string]bool) 73 | queued[source] = true 74 | 75 | for { 76 | node := queue[0] 77 | queue = queue[1:] 78 | findNewLink := false 79 | for _, link := range topology { 80 | if link.A == node { 81 | if !queued[link.B] { 82 | queue = append(queue, link.B) 83 | queued[link.B] = true 84 | findNewLink = true 85 | } 86 | if !visited[link.B] { 87 | findNewLink = true 88 | } 89 | } 90 | if link.B == node { 91 | if !queued[link.A] { 92 | queue = append(queue, link.A) 93 | queued[link.A] = true 94 | findNewLink = true 95 | } 96 | if !visited[link.A] { 97 | findNewLink = true 98 | } 99 | } 100 | } 101 | visited[node] = true 102 | if !findNewLink { 103 | logger.InitLog.Debugf("%s is Anchor UPF", node) 104 | upList = append(upList, node) 105 | } 106 | if len(queue) == 0 { 107 | break 108 | } 109 | } 110 | if len(upList) == 0 { 111 | return nil, errors.New("Not found Anchor UPF in topology") 112 | } 113 | sort.Strings(upList) 114 | return upList, nil 115 | } 116 | 117 | func generateDefaultDataPath(source string, destination string, topology []factory.UPLink) (*DataPath, error) { 118 | allPaths, _ := getAllPathByNodeName(source, destination, topology) 119 | if len(allPaths) == 0 { 120 | return nil, fmt.Errorf("Path not exist: %s to %s", source, destination) 121 | } 122 | 123 | dataPath := NewDataPath() 124 | lowerBound := 0 125 | var parentNode *DataPathNode = nil 126 | 127 | // if multiple Paths exist, select one according to some criterion 128 | for idx, nodeName := range allPaths[0] { 129 | newUeNode, err := NewUEDataPathNode(nodeName) 130 | if err != nil { 131 | return nil, err 132 | } 133 | if idx == lowerBound { 134 | dataPath.FirstDPNode = newUeNode 135 | } 136 | if parentNode != nil { 137 | newUeNode.AddPrev(parentNode) 138 | parentNode.AddNext(newUeNode) 139 | } 140 | parentNode = newUeNode 141 | } 142 | logger.CtxLog.Tracef("New default data path (%s to %s): ", source, destination) 143 | logger.CtxLog.Traceln("\n" + dataPath.String() + "\n") 144 | return dataPath, nil 145 | } 146 | 147 | func getAllPathByNodeName(src, dest string, links []factory.UPLink) (map[int][]string, int) { 148 | visited := make(map[string]bool) 149 | allPaths := make(map[int][]string) 150 | count := 0 151 | var findPath func(src, dest string, links []factory.UPLink, currentPath []string) 152 | 153 | findPath = func(src, dest string, links []factory.UPLink, currentPath []string) { 154 | if visited[src] { 155 | return 156 | } 157 | visited[src] = true 158 | currentPath = append(currentPath, src) 159 | logger.InitLog.Traceln("current path:", currentPath) 160 | if src == dest { 161 | cpy := make([]string, len(currentPath)) 162 | copy(cpy, currentPath) 163 | allPaths[count] = cpy[1:] 164 | count++ 165 | logger.InitLog.Traceln("all path:", allPaths) 166 | visited[src] = false 167 | return 168 | } 169 | for _, link := range links { 170 | // search A to B only 171 | if link.A == src { 172 | findPath(link.B, dest, links, currentPath) 173 | } 174 | } 175 | visited[src] = false 176 | } 177 | 178 | findPath(src, dest, links, []string{}) 179 | return allPaths, count 180 | } 181 | 182 | func createUPFListForSelectionULCL(inputList []string) (outputList []string) { 183 | offset := rand.Intn(len(inputList)) 184 | return append(inputList[offset:], inputList[:offset]...) 185 | } 186 | 187 | func (dfp *UEDefaultPaths) SelectUPFAndAllocUEIPForULCL(upi *UserPlaneInformation, 188 | selection *UPFSelectionParams, 189 | ) (string, net.IP, bool) { 190 | sortedUPFList := createUPFListForSelectionULCL(dfp.AnchorUPFs) 191 | 192 | for _, upfName := range sortedUPFList { 193 | logger.CtxLog.Debugf("check start UPF: %s", upfName) 194 | upf := upi.UPFs[upfName] 195 | 196 | pools, useStaticIPPool := getUEIPPool(upf, selection) 197 | if len(pools) == 0 { 198 | continue 199 | } 200 | sortedPoolList := createPoolListForSelection(pools) 201 | for _, pool := range sortedPoolList { 202 | logger.CtxLog.Debugf("check start UEIPPool(%+v)", pool.ueSubNet) 203 | addr := pool.Allocate(selection.PDUAddress) 204 | if addr != nil { 205 | logger.CtxLog.Infof("Selected UPF: %s", upfName) 206 | return upfName, addr, useStaticIPPool 207 | } 208 | // if all addresses in pool are used, search next pool 209 | logger.CtxLog.Debug("check next pool") 210 | } 211 | // if all addresses in UPF are used, search next UPF 212 | logger.CtxLog.Debug("check next upf") 213 | } 214 | // checked all UPFs 215 | logger.CtxLog.Warnf("UE IP pool exhausted for DNN[%s] S-NSSAI[sst: %d sd: %s] DNAI[%s]\n", selection.Dnn, 216 | selection.SNssai.Sst, selection.SNssai.Sd, selection.Dnai) 217 | return "", nil, false 218 | } 219 | 220 | func (dfp *UEDefaultPaths) GetDefaultPath(upfName string) *DataPath { 221 | firstNode := dfp.DefaultPathPool[upfName].CopyFirstDPNode() 222 | dataPath := &DataPath{ 223 | Activated: false, 224 | IsDefaultPath: true, 225 | Destination: dfp.DefaultPathPool[upfName].Destination, 226 | FirstDPNode: firstNode, 227 | } 228 | return dataPath 229 | } 230 | -------------------------------------------------------------------------------- /internal/context/ue_ip_pool.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/free5gc/smf/internal/context/pool" 10 | "github.com/free5gc/smf/internal/logger" 11 | "github.com/free5gc/smf/pkg/factory" 12 | ) 13 | 14 | // UeIPPool represent IPv4 address pool for UE 15 | type UeIPPool struct { 16 | ueSubNet *net.IPNet 17 | pool *pool.LazyReusePool 18 | } 19 | 20 | func NewUEIPPool(factoryPool *factory.UEIPPool) *UeIPPool { 21 | _, ipNet, err := net.ParseCIDR(factoryPool.Cidr) 22 | if err != nil { 23 | logger.InitLog.Errorln(err) 24 | return nil 25 | } 26 | 27 | minAddr, maxAddr, err := calcAddrRange(ipNet) 28 | if err != nil { 29 | logger.InitLog.Errorln(err) 30 | return nil 31 | } 32 | 33 | newPool, err := pool.NewLazyReusePool(int(minAddr), int(maxAddr)) 34 | if err != nil { 35 | logger.InitLog.Errorln(err) 36 | return nil 37 | } 38 | 39 | ueIPPool := &UeIPPool{ 40 | ueSubNet: ipNet, 41 | pool: newPool, 42 | } 43 | return ueIPPool 44 | } 45 | 46 | func (ueIPPool *UeIPPool) Allocate(request net.IP) net.IP { 47 | var allocVal int 48 | var ok bool 49 | if request != nil { 50 | allocVal = int(binary.BigEndian.Uint32(request)) 51 | ok = ueIPPool.pool.Use(allocVal) 52 | if !ok { 53 | logger.CtxLog.Warnf("IP[%s] is used in Pool[%+v]", request, ueIPPool.ueSubNet) 54 | return nil 55 | } 56 | // if allocated request IP address 57 | goto RETURNIP 58 | } 59 | 60 | allocVal, ok = ueIPPool.pool.Allocate() 61 | if !ok { 62 | logger.CtxLog.Warnf("Pool is empty: %+v", ueIPPool.ueSubNet) 63 | return nil 64 | } 65 | 66 | RETURNIP: 67 | retIP := uint32ToIP(uint32(allocVal)) 68 | logger.CtxLog.Infof("Allocated UE IP address: %s", retIP) 69 | return retIP 70 | } 71 | 72 | func (ueIPPool *UeIPPool) Exclude(excludePool *UeIPPool) error { 73 | excludeMin := excludePool.pool.Min() 74 | excludeMax := excludePool.pool.Max() 75 | if err := ueIPPool.pool.Reserve(excludeMin, excludeMax); err != nil { 76 | return fmt.Errorf("exclude uePool fail: %v", err) 77 | } 78 | return nil 79 | } 80 | 81 | func (u *UeIPPool) Pool() *pool.LazyReusePool { 82 | return u.pool 83 | } 84 | 85 | func uint32ToIP(intval uint32) net.IP { 86 | buf := make([]byte, 4) 87 | binary.BigEndian.PutUint32(buf, intval) 88 | return buf 89 | } 90 | 91 | func (ueIPPool *UeIPPool) Release(addr net.IP) { 92 | addrVal := binary.BigEndian.Uint32(addr) 93 | res := ueIPPool.pool.Free(int(addrVal)) 94 | if !res { 95 | logger.CtxLog.Warnf("failed to release UE Address: %s", addr) 96 | } 97 | logger.CtxLog.Debug(ueIPPool.dump()) 98 | } 99 | 100 | func (ueIPPool *UeIPPool) dump() string { 101 | str := "[" 102 | elements := ueIPPool.pool.Dump() 103 | for index, element := range elements { 104 | var firstAddr net.IP 105 | var lastAddr net.IP 106 | buf := make([]byte, 4) 107 | binary.BigEndian.PutUint32(buf, uint32(element[0])) 108 | firstAddr = buf 109 | buf = make([]byte, 4) 110 | binary.BigEndian.PutUint32(buf, uint32(element[1])) 111 | lastAddr = buf 112 | if index > 0 { 113 | str += ("->") 114 | } 115 | str += fmt.Sprintf("{%s - %s}", firstAddr.String(), lastAddr.String()) 116 | } 117 | str += ("]") 118 | return str 119 | } 120 | 121 | func isOverlap(pools []*UeIPPool) bool { 122 | if len(pools) < 2 { 123 | // no need to check 124 | return false 125 | } 126 | for i := 0; i < len(pools)-1; i++ { 127 | for j := i + 1; j < len(pools); j++ { 128 | if pools[i].pool.IsJoint(pools[j].pool) { 129 | return true 130 | } 131 | } 132 | } 133 | return false 134 | } 135 | 136 | func calcAddrRange(ipNet *net.IPNet) (minAddr, maxAddr uint32, err error) { 137 | maskVal := binary.BigEndian.Uint32(ipNet.Mask) 138 | baseIPVal := binary.BigEndian.Uint32(ipNet.IP) 139 | // move removing network and broadcast address later 140 | minAddr = (baseIPVal & maskVal) 141 | maxAddr = (baseIPVal | ^maskVal) 142 | if minAddr > maxAddr { 143 | return minAddr, maxAddr, errors.New("Mask is invalid.") 144 | } 145 | return minAddr, maxAddr, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/context/ue_ip_pool_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/free5gc/smf/internal/context" 12 | "github.com/free5gc/smf/pkg/factory" 13 | ) 14 | 15 | func TestUeIPPool(t *testing.T) { 16 | ueIPPool := context.NewUEIPPool(&factory.UEIPPool{ 17 | Cidr: "10.10.0.0/24", 18 | }) 19 | 20 | require.NotNil(t, ueIPPool) 21 | 22 | var allocIP net.IP 23 | 24 | // make allowed ip pools 25 | var ipPoolList []net.IP 26 | for i := 0; i <= 255; i += 1 { 27 | ipStr := fmt.Sprintf("10.10.0.%d", i) 28 | ipPoolList = append(ipPoolList, net.ParseIP(ipStr).To4()) 29 | } 30 | 31 | // allocate 32 | for i := 0; i < 256; i += 1 { 33 | allocIP = ueIPPool.Allocate(nil) 34 | require.Contains(t, ipPoolList, allocIP) 35 | } 36 | 37 | // ip pool is empty 38 | allocIP = ueIPPool.Allocate(nil) 39 | require.Nil(t, allocIP) 40 | 41 | // release IP 42 | for _, i := range rand.Perm(256) { 43 | ueIPPool.Release(ipPoolList[i]) 44 | } 45 | 46 | // allocate specify ip 47 | for _, ip := range ipPoolList { 48 | allocIP = ueIPPool.Allocate(ip) 49 | require.Equal(t, ip, allocIP) 50 | } 51 | } 52 | 53 | func TestUeIPPool_ExcludeRange(t *testing.T) { 54 | ueIPPool := context.NewUEIPPool(&factory.UEIPPool{ 55 | Cidr: "10.10.0.0/24", 56 | }) 57 | 58 | require.Equal(t, 0x0a0a0000, ueIPPool.Pool().Min()) 59 | require.Equal(t, 0x0a0a00FF, ueIPPool.Pool().Max()) 60 | require.Equal(t, 256, ueIPPool.Pool().Remain()) 61 | 62 | excludeUeIPPool := context.NewUEIPPool(&factory.UEIPPool{ 63 | Cidr: "10.10.0.0/28", 64 | }) 65 | 66 | require.Equal(t, 0x0a0a0000, excludeUeIPPool.Pool().Min()) 67 | require.Equal(t, 0x0a0a000F, excludeUeIPPool.Pool().Max()) 68 | 69 | require.Equal(t, 16, excludeUeIPPool.Pool().Remain()) 70 | 71 | err := ueIPPool.Exclude(excludeUeIPPool) 72 | require.NoError(t, err) 73 | require.Equal(t, 240, ueIPPool.Pool().Remain()) 74 | 75 | for i := 16; i <= 255; i++ { 76 | allocate := ueIPPool.Allocate(nil) 77 | require.Equal(t, net.ParseIP(fmt.Sprintf("10.10.0.%d", i)).To4(), allocate) 78 | 79 | ueIPPool.Release(allocate) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/context/ulcl_group.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | func GetULCLGroupNameFromSUPI(supi string) string { 4 | ulclGroups := smfContext.ULCLGroups 5 | for name, group := range ulclGroups { 6 | for _, member := range group { 7 | if member == supi { 8 | return name 9 | } 10 | } 11 | } 12 | return "" 13 | } 14 | -------------------------------------------------------------------------------- /internal/context/upf_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "testing" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | 11 | "github.com/free5gc/nas/nasMessage" 12 | "github.com/free5gc/pfcp/pfcpType" 13 | smf_context "github.com/free5gc/smf/internal/context" 14 | "github.com/free5gc/smf/pkg/factory" 15 | ) 16 | 17 | var mockIPv4NodeID = &pfcpType.NodeID{ 18 | NodeIdType: pfcpType.NodeIdTypeIpv4Address, 19 | IP: net.ParseIP("127.0.0.1"), 20 | } 21 | 22 | var mockIfaces = []*factory.InterfaceUpfInfoItem{ 23 | { 24 | InterfaceType: "N3", 25 | Endpoints: []string{"127.0.0.1"}, 26 | NetworkInstances: []string{"internet"}, 27 | }, 28 | } 29 | 30 | func convertPDUSessTypeToString(pduType uint8) string { 31 | switch pduType { 32 | case nasMessage.PDUSessionTypeIPv4: 33 | return "PDU Session Type IPv4" 34 | case nasMessage.PDUSessionTypeIPv6: 35 | return "PDU Session Type IPv6" 36 | case nasMessage.PDUSessionTypeIPv4IPv6: 37 | return "PDU Session Type IPv4 IPv6" 38 | case nasMessage.PDUSessionTypeUnstructured: 39 | return "PDU Session Type Unstructured" 40 | case nasMessage.PDUSessionTypeEthernet: 41 | return "PDU Session Type Ethernet" 42 | } 43 | 44 | return "Unkwown PDU Session Type" 45 | } 46 | 47 | func TestIP(t *testing.T) { 48 | testCases := []struct { 49 | input *smf_context.UPFInterfaceInfo 50 | inputPDUSessionType uint8 51 | paramStr string 52 | resultStr string 53 | expectedIP string 54 | expectedError error 55 | }{ 56 | { 57 | input: &smf_context.UPFInterfaceInfo{ 58 | NetworkInstances: []string{""}, 59 | IPv4EndPointAddresses: []net.IP{net.ParseIP("8.8.8.8")}, 60 | IPv6EndPointAddresses: []net.IP{net.ParseIP("2001:4860:4860::8888")}, 61 | EndpointFQDN: "www.google.com", 62 | }, 63 | inputPDUSessionType: nasMessage.PDUSessionTypeIPv4, 64 | paramStr: "select " + convertPDUSessTypeToString(nasMessage.PDUSessionTypeIPv4), 65 | expectedIP: "8.8.8.8", 66 | expectedError: nil, 67 | }, 68 | { 69 | input: &smf_context.UPFInterfaceInfo{ 70 | NetworkInstances: []string{""}, 71 | IPv4EndPointAddresses: []net.IP{net.ParseIP("8.8.8.8")}, 72 | IPv6EndPointAddresses: []net.IP{net.ParseIP("2001:4860:4860::8888")}, 73 | EndpointFQDN: "www.google.com", 74 | }, 75 | inputPDUSessionType: nasMessage.PDUSessionTypeIPv6, 76 | paramStr: "select " + convertPDUSessTypeToString(nasMessage.PDUSessionTypeIPv6), 77 | expectedIP: "2001:4860:4860::8888", 78 | expectedError: nil, 79 | }, 80 | } 81 | 82 | Convey("Given UPFInterfaceInfo and select PDU Session type, should return correct IP", t, func() { 83 | for i, testcase := range testCases { 84 | upfInterfaceInfo := testcase.input 85 | infoStr := fmt.Sprintf("testcase[%d] UPF Interface Info: %+v", i, upfInterfaceInfo) 86 | 87 | Convey(infoStr, func() { 88 | Convey(testcase.paramStr, func() { 89 | ip, err := upfInterfaceInfo.IP(testcase.inputPDUSessionType) 90 | testcase.resultStr = "IP addr should be " + testcase.expectedIP 91 | 92 | Convey(testcase.resultStr, func() { 93 | So(ip.String(), ShouldEqual, testcase.expectedIP) 94 | So(err, ShouldEqual, testcase.expectedError) 95 | }) 96 | }) 97 | }) 98 | } 99 | }) 100 | } 101 | 102 | func TestAddDataPath(t *testing.T) { 103 | // AddDataPath is simple, should only have one case 104 | testCases := []struct { 105 | tunnel *smf_context.UPTunnel 106 | addedDataPath *smf_context.DataPath 107 | resultStr string 108 | expectedExist bool 109 | }{ 110 | { 111 | tunnel: smf_context.NewUPTunnel(), 112 | addedDataPath: smf_context.NewDataPath(), 113 | resultStr: "Datapath should exist", 114 | expectedExist: true, 115 | }, 116 | } 117 | 118 | Convey("AddDataPath should indeed add datapath", t, func() { 119 | for i, testcase := range testCases { 120 | upTunnel := testcase.tunnel 121 | infoStr := fmt.Sprintf("testcase[%d]: Add Datapath", i) 122 | 123 | Convey(infoStr, func() { 124 | upTunnel.AddDataPath(testcase.addedDataPath) 125 | 126 | Convey(testcase.resultStr, func() { 127 | var exist bool 128 | for _, datapath := range upTunnel.DataPathPool { 129 | if datapath == testcase.addedDataPath { 130 | exist = true 131 | } 132 | } 133 | So(exist, ShouldEqual, testcase.expectedExist) 134 | }) 135 | }) 136 | } 137 | }) 138 | } 139 | 140 | func TestAddPDR(t *testing.T) { 141 | testCases := []struct { 142 | upf *smf_context.UPF 143 | resultStr string 144 | expectedError error 145 | }{ 146 | { 147 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 148 | resultStr: "AddPDR should success", 149 | expectedError: nil, 150 | }, 151 | { 152 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 153 | resultStr: "AddPDR should fail", 154 | expectedError: fmt.Errorf("UPF[127.0.0.1] not associated with SMF"), 155 | }, 156 | } 157 | 158 | testCases[0].upf.AssociationContext = context.Background() 159 | 160 | Convey("AddPDR should indeed add PDR and report error appropiately", t, func() { 161 | for i, testcase := range testCases { 162 | upf := testcase.upf 163 | infoStr := fmt.Sprintf("testcase[%d]: ", i) 164 | 165 | Convey(infoStr, func() { 166 | _, err := upf.AddPDR() 167 | 168 | Convey(testcase.resultStr, func() { 169 | if testcase.expectedError == nil { 170 | So(err, ShouldBeNil) 171 | } else { 172 | So(err, ShouldNotBeNil) 173 | if err != nil { 174 | So(err.Error(), ShouldEqual, testcase.expectedError.Error()) 175 | } 176 | } 177 | }) 178 | }) 179 | } 180 | }) 181 | } 182 | 183 | func TestAddFAR(t *testing.T) { 184 | testCases := []struct { 185 | upf *smf_context.UPF 186 | resultStr string 187 | expectedError error 188 | }{ 189 | { 190 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 191 | resultStr: "AddFAR should success", 192 | expectedError: nil, 193 | }, 194 | { 195 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 196 | resultStr: "AddFAR should fail", 197 | expectedError: fmt.Errorf("UPF[127.0.0.1] not associated with SMF"), 198 | }, 199 | } 200 | 201 | testCases[0].upf.AssociationContext = context.Background() 202 | 203 | Convey("AddFAR should indeed add FAR and report error appropiately", t, func() { 204 | for i, testcase := range testCases { 205 | upf := testcase.upf 206 | infoStr := fmt.Sprintf("testcase[%d]: ", i) 207 | 208 | Convey(infoStr, func() { 209 | _, err := upf.AddFAR() 210 | 211 | Convey(testcase.resultStr, func() { 212 | if testcase.expectedError == nil { 213 | So(err, ShouldBeNil) 214 | } else { 215 | So(err, ShouldNotBeNil) 216 | if err != nil { 217 | So(err.Error(), ShouldEqual, testcase.expectedError.Error()) 218 | } 219 | } 220 | }) 221 | }) 222 | } 223 | }) 224 | } 225 | 226 | func TestAddQER(t *testing.T) { 227 | testCases := []struct { 228 | upf *smf_context.UPF 229 | resultStr string 230 | expectedError error 231 | }{ 232 | { 233 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 234 | resultStr: "AddQER should success", 235 | expectedError: nil, 236 | }, 237 | { 238 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 239 | resultStr: "AddQER should fail", 240 | expectedError: fmt.Errorf("UPF[127.0.0.1] not associated with SMF"), 241 | }, 242 | } 243 | 244 | testCases[0].upf.AssociationContext = context.Background() 245 | 246 | Convey("AddQER should indeed add QER and report error appropiately", t, func() { 247 | for i, testcase := range testCases { 248 | upf := testcase.upf 249 | infoStr := fmt.Sprintf("testcase[%d]: ", i) 250 | 251 | Convey(infoStr, func() { 252 | _, err := upf.AddQER() 253 | 254 | Convey(testcase.resultStr, func() { 255 | if testcase.expectedError == nil { 256 | So(err, ShouldBeNil) 257 | } else { 258 | So(err, ShouldNotBeNil) 259 | if err != nil { 260 | So(err.Error(), ShouldEqual, testcase.expectedError.Error()) 261 | } 262 | } 263 | }) 264 | }) 265 | } 266 | }) 267 | } 268 | 269 | func TestAddBAR(t *testing.T) { 270 | testCases := []struct { 271 | upf *smf_context.UPF 272 | resultStr string 273 | expectedError error 274 | }{ 275 | { 276 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 277 | resultStr: "AddBAR should success", 278 | expectedError: nil, 279 | }, 280 | { 281 | upf: smf_context.NewUPF(mockIPv4NodeID, mockIfaces), 282 | resultStr: "AddBAR should fail", 283 | expectedError: fmt.Errorf("UPF[127.0.0.1] not associated with SMF"), 284 | }, 285 | } 286 | 287 | testCases[0].upf.AssociationContext = context.Background() 288 | 289 | Convey("AddBAR should indeed add BAR and report error appropiately", t, func() { 290 | for i, testcase := range testCases { 291 | upf := testcase.upf 292 | infoStr := fmt.Sprintf("testcase[%d]: ", i) 293 | 294 | Convey(infoStr, func() { 295 | _, err := upf.AddBAR() 296 | 297 | Convey(testcase.resultStr, func() { 298 | if testcase.expectedError == nil { 299 | So(err, ShouldBeNil) 300 | } else { 301 | So(err, ShouldNotBeNil) 302 | if err != nil { 303 | So(err.Error(), ShouldEqual, testcase.expectedError.Error()) 304 | } 305 | } 306 | }) 307 | }) 308 | } 309 | }) 310 | } 311 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | logger_util "github.com/free5gc/util/logger" 7 | ) 8 | 9 | const ( 10 | FieldSupi = "supi" 11 | FieldPDUSessionID = "pdu_session_id" 12 | ) 13 | 14 | var ( 15 | Log *logrus.Logger 16 | NfLog *logrus.Entry 17 | MainLog *logrus.Entry 18 | InitLog *logrus.Entry 19 | CfgLog *logrus.Entry 20 | CtxLog *logrus.Entry 21 | GinLog *logrus.Entry 22 | SBILog *logrus.Entry 23 | ConsumerLog *logrus.Entry 24 | GsmLog *logrus.Entry 25 | PfcpLog *logrus.Entry 26 | PduSessLog *logrus.Entry 27 | ChargingLog *logrus.Entry 28 | UtilLog *logrus.Entry 29 | ) 30 | 31 | func init() { 32 | fieldsOrder := []string{ 33 | logger_util.FieldNF, 34 | logger_util.FieldCategory, 35 | } 36 | 37 | Log = logger_util.New(fieldsOrder) 38 | NfLog = Log.WithField(logger_util.FieldNF, "SMF") 39 | MainLog = NfLog.WithField(logger_util.FieldCategory, "Main") 40 | InitLog = NfLog.WithField(logger_util.FieldCategory, "Init") 41 | CfgLog = NfLog.WithField(logger_util.FieldCategory, "CFG") 42 | CtxLog = NfLog.WithField(logger_util.FieldCategory, "CTX") 43 | GinLog = NfLog.WithField(logger_util.FieldCategory, "GIN") 44 | SBILog = NfLog.WithField(logger_util.FieldCategory, "SBI") 45 | ConsumerLog = NfLog.WithField(logger_util.FieldCategory, "Consumer") 46 | GsmLog = NfLog.WithField(logger_util.FieldCategory, "GSM") 47 | PfcpLog = NfLog.WithField(logger_util.FieldCategory, "PFCP") 48 | PduSessLog = NfLog.WithField(logger_util.FieldCategory, "PduSess") 49 | ChargingLog = NfLog.WithField(logger_util.FieldCategory, "Charging") 50 | UtilLog = NfLog.WithField(logger_util.FieldCategory, "Util") 51 | } 52 | -------------------------------------------------------------------------------- /internal/pfcp/dispatcher.go: -------------------------------------------------------------------------------- 1 | package pfcp 2 | 3 | import ( 4 | "github.com/free5gc/pfcp" 5 | "github.com/free5gc/pfcp/pfcpUdp" 6 | "github.com/free5gc/smf/internal/logger" 7 | "github.com/free5gc/smf/internal/pfcp/handler" 8 | ) 9 | 10 | func Dispatch(msg *pfcpUdp.Message) { 11 | switch msg.PfcpMessage.Header.MessageType { 12 | case pfcp.PFCP_HEARTBEAT_REQUEST: 13 | handler.HandlePfcpHeartbeatRequest(msg) 14 | case pfcp.PFCP_PFD_MANAGEMENT_REQUEST: 15 | handler.HandlePfcpPfdManagementRequest(msg) 16 | case pfcp.PFCP_ASSOCIATION_SETUP_REQUEST: 17 | handler.HandlePfcpAssociationSetupRequest(msg) 18 | case pfcp.PFCP_ASSOCIATION_UPDATE_REQUEST: 19 | handler.HandlePfcpAssociationUpdateRequest(msg) 20 | case pfcp.PFCP_ASSOCIATION_RELEASE_REQUEST: 21 | handler.HandlePfcpAssociationReleaseRequest(msg) 22 | case pfcp.PFCP_NODE_REPORT_REQUEST: 23 | handler.HandlePfcpNodeReportRequest(msg) 24 | case pfcp.PFCP_SESSION_SET_DELETION_REQUEST: 25 | handler.HandlePfcpSessionSetDeletionRequest(msg) 26 | case pfcp.PFCP_SESSION_REPORT_REQUEST: 27 | handler.HandlePfcpSessionReportRequest(msg) 28 | default: 29 | logger.PfcpLog.Errorf("Unknown PFCP message type: %d", msg.PfcpMessage.Header.MessageType) 30 | return 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/pfcp/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/free5gc/openapi/models" 7 | "github.com/free5gc/pfcp" 8 | "github.com/free5gc/pfcp/pfcpType" 9 | "github.com/free5gc/pfcp/pfcpUdp" 10 | smf_context "github.com/free5gc/smf/internal/context" 11 | "github.com/free5gc/smf/internal/logger" 12 | pfcp_message "github.com/free5gc/smf/internal/pfcp/message" 13 | "github.com/free5gc/smf/pkg/service" 14 | ) 15 | 16 | func HandlePfcpHeartbeatRequest(msg *pfcpUdp.Message) { 17 | h := msg.PfcpMessage.Header 18 | pfcp_message.SendHeartbeatResponse(msg.RemoteAddr, h.SequenceNumber) 19 | } 20 | 21 | func HandlePfcpPfdManagementRequest(msg *pfcpUdp.Message) { 22 | logger.PfcpLog.Warnf("PFCP PFD Management Request handling is not implemented") 23 | } 24 | 25 | func HandlePfcpAssociationSetupRequest(msg *pfcpUdp.Message) { 26 | req := msg.PfcpMessage.Body.(pfcp.PFCPAssociationSetupRequest) 27 | 28 | nodeID := req.NodeID 29 | if nodeID == nil { 30 | logger.PfcpLog.Errorln("pfcp association needs NodeID") 31 | return 32 | } 33 | logger.PfcpLog.Infof("Handle PFCP Association Setup Request with NodeID[%s]", 34 | nodeID.ResolveNodeIdToIp().String()) 35 | 36 | upf := smf_context.RetrieveUPFNodeByNodeID(*nodeID) 37 | if upf == nil { 38 | logger.PfcpLog.Errorf("can't find UPF[%s]", nodeID.ResolveNodeIdToIp().String()) 39 | return 40 | } 41 | 42 | // Response with PFCP Association Setup Response 43 | cause := pfcpType.Cause{ 44 | CauseValue: pfcpType.CauseRequestAccepted, 45 | } 46 | pfcp_message.SendPfcpAssociationSetupResponse(msg.RemoteAddr, cause) 47 | } 48 | 49 | func HandlePfcpAssociationUpdateRequest(msg *pfcpUdp.Message) { 50 | logger.PfcpLog.Warnf("PFCP Association Update Request handling is not implemented") 51 | } 52 | 53 | func HandlePfcpAssociationReleaseRequest(msg *pfcpUdp.Message) { 54 | pfcpMsg := msg.PfcpMessage.Body.(pfcp.PFCPAssociationReleaseRequest) 55 | 56 | var cause pfcpType.Cause 57 | upf := smf_context.RetrieveUPFNodeByNodeID(*pfcpMsg.NodeID) 58 | 59 | if upf != nil { 60 | smf_context.RemoveUPFNodeByNodeID(*pfcpMsg.NodeID) 61 | cause.CauseValue = pfcpType.CauseRequestAccepted 62 | } else { 63 | cause.CauseValue = pfcpType.CauseNoEstablishedPfcpAssociation 64 | } 65 | 66 | pfcp_message.SendPfcpAssociationReleaseResponse(msg.RemoteAddr, cause) 67 | } 68 | 69 | func HandlePfcpNodeReportRequest(msg *pfcpUdp.Message) { 70 | logger.PfcpLog.Warnf("PFCP Node Report Request handling is not implemented") 71 | } 72 | 73 | func HandlePfcpSessionSetDeletionRequest(msg *pfcpUdp.Message) { 74 | logger.PfcpLog.Warnf("PFCP Session Set Deletion Request handling is not implemented") 75 | } 76 | 77 | func HandlePfcpSessionSetDeletionResponse(msg *pfcpUdp.Message) { 78 | logger.PfcpLog.Warnf("PFCP Session Set Deletion Response handling is not implemented") 79 | } 80 | 81 | func HandlePfcpSessionReportRequest(msg *pfcpUdp.Message) { 82 | var cause pfcpType.Cause 83 | 84 | req := msg.PfcpMessage.Body.(pfcp.PFCPSessionReportRequest) 85 | SEID := msg.PfcpMessage.Header.SEID 86 | smContext := smf_context.GetSMContextBySEID(SEID) 87 | seqFromUPF := msg.PfcpMessage.Header.SequenceNumber 88 | 89 | if smContext == nil { 90 | logger.PfcpLog.Errorf("PFCP Session SEID[%d] not found", SEID) 91 | cause.CauseValue = pfcpType.CauseSessionContextNotFound 92 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, 0) 93 | return 94 | } 95 | 96 | smContext.SMLock.Lock() 97 | defer smContext.SMLock.Unlock() 98 | 99 | upfNodeID := smContext.GetNodeIDByLocalSEID(SEID) 100 | upfNodeIDtoIP := upfNodeID.ResolveNodeIdToIp() 101 | if upfNodeIDtoIP.IsUnspecified() { 102 | logger.PduSessLog.Errorf("Invalid PFCP Session Report Request : no PFCP session found with SEID %d", SEID) 103 | cause.CauseValue = pfcpType.CauseNoEstablishedPfcpAssociation 104 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, 0) 105 | return 106 | } 107 | upfNodeIDtoIPStr := upfNodeIDtoIP.String() 108 | 109 | pfcpCtx := smContext.PFCPContext[upfNodeIDtoIPStr] 110 | if pfcpCtx == nil { 111 | logger.PfcpLog.Errorf("pfcpCtx [nodeId: %v, seid:%d] not found", upfNodeID, SEID) 112 | cause.CauseValue = pfcpType.CauseNoEstablishedPfcpAssociation 113 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, 0) 114 | return 115 | } 116 | remoteSEID := pfcpCtx.RemoteSEID 117 | 118 | upf := smf_context.RetrieveUPFNodeByNodeID(upfNodeID) 119 | if upf == nil { 120 | logger.PfcpLog.Errorf("can't find UPF[%s]", upfNodeIDtoIPStr) 121 | cause.CauseValue = pfcpType.CauseNoEstablishedPfcpAssociation 122 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, 0) 123 | return 124 | } 125 | if err := upf.IsAssociated(); err != nil { 126 | logger.PfcpLog.Warnf("PFCP Session Report Request rejected: %+v", err) 127 | cause.CauseValue = pfcpType.CauseNoEstablishedPfcpAssociation 128 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, 0) 129 | } 130 | 131 | if smContext.UpCnxState == models.UpCnxState_DEACTIVATED { 132 | if req.ReportType.Dldr { 133 | downlinkDataReport := req.DownlinkDataReport 134 | 135 | if downlinkDataReport.DownlinkDataServiceInformation != nil { 136 | logger.PfcpLog.Warnf( 137 | "PFCP Session Report Request DownlinkDataServiceInformation handling is not implemented") 138 | } 139 | 140 | n1n2Request := models.N1N2MessageTransferRequest{} 141 | 142 | // TS 23.502 4.2.3.3 3a. Send Namf_Communication_N1N2MessageTransfer Request, SMF->AMF 143 | if n2SmBuf, err := smf_context.BuildPDUSessionResourceSetupRequestTransfer(smContext); err != nil { 144 | logger.PduSessLog.Errorln("Build PDUSessionResourceSetupRequestTransfer failed:", err) 145 | } else { 146 | n1n2Request.BinaryDataN2Information = n2SmBuf 147 | } 148 | 149 | n1n2Request.JsonData = &models.N1N2MessageTransferReqData{ 150 | PduSessionId: smContext.PDUSessionID, 151 | // Temporarily assign SMF itself, 152 | // TODO: TS 23.502 4.2.3.3 5. Namf_Communication_N1N2TransferFailureNotification 153 | N1n2FailureTxfNotifURI: fmt.Sprintf("%s://%s:%d", 154 | smf_context.GetSelf().URIScheme, 155 | smf_context.GetSelf().RegisterIPv4, 156 | smf_context.GetSelf().SBIPort), 157 | N2InfoContainer: &models.N2InfoContainer{ 158 | N2InformationClass: models.N2InformationClass_SM, 159 | SmInfo: &models.N2SmInformation{ 160 | PduSessionId: smContext.PDUSessionID, 161 | N2InfoContent: &models.N2InfoContent{ 162 | NgapIeType: models.AmfCommunicationNgapIeType_PDU_RES_SETUP_REQ, 163 | NgapData: &models.RefToBinaryData{ 164 | ContentId: "N2SmInformation", 165 | }, 166 | }, 167 | SNssai: smContext.SNssai, 168 | }, 169 | }, 170 | } 171 | 172 | ctx, _, errToken := smf_context.GetSelf().GetTokenCtx(models.ServiceName_NAMF_COMM, models.NrfNfManagementNfType_AMF) 173 | if errToken != nil { 174 | logger.PfcpLog.Warnf("Get NAMF_COMM context failed: %s", errToken) 175 | return 176 | } 177 | rspData, err := service.GetApp().Consumer(). 178 | N1N2MessageTransfer(ctx, smContext.Supi, n1n2Request, smContext.CommunicationClientApiPrefix) 179 | if err != nil { 180 | logger.ConsumerLog.Warnf("Send N1N2Transfer failed: %s", err) 181 | return 182 | } 183 | 184 | if rspData.Cause == models.N1N2MessageTransferCause_ATTEMPTING_TO_REACH_UE { 185 | logger.PfcpLog.Infof("Receive %v, AMF is able to page the UE", rspData.Cause) 186 | } 187 | if rspData.Cause == models.N1N2MessageTransferCause_UE_NOT_RESPONDING { 188 | logger.PfcpLog.Warnf("%v", rspData.Cause) 189 | // TODO: TS 23.502 4.2.3.3 3c. Failure indication 190 | } 191 | } 192 | } 193 | 194 | if req.ReportType.Usar && req.UsageReport != nil { 195 | smContext.HandleReports(req.UsageReport, nil, nil, upfNodeID, "") 196 | // After receiving the Usage Report, it should send charging request to the CHF 197 | // and update the URR with the quota or other charging information according to 198 | // the charging response 199 | service.GetApp().Processor().ReportUsageAndUpdateQuota(smContext) 200 | } 201 | 202 | // TS 23.502 4.2.3.3 2b. Send Data Notification Ack, SMF->UPF 203 | cause.CauseValue = pfcpType.CauseRequestAccepted 204 | pfcp_message.SendPfcpSessionReportResponse(msg.RemoteAddr, cause, seqFromUPF, remoteSEID) 205 | } 206 | -------------------------------------------------------------------------------- /internal/pfcp/handler/handler_test.go: -------------------------------------------------------------------------------- 1 | package handler_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/sirupsen/logrus" 12 | . "github.com/smartystreets/goconvey/convey" 13 | 14 | "github.com/free5gc/pfcp" 15 | "github.com/free5gc/pfcp/pfcpType" 16 | "github.com/free5gc/pfcp/pfcpUdp" 17 | "github.com/free5gc/smf/internal/logger" 18 | "github.com/free5gc/smf/internal/pfcp/handler" 19 | ) 20 | 21 | type LogCapture struct { 22 | buffer bytes.Buffer 23 | } 24 | 25 | func (lc *LogCapture) Write(p []byte) (n int, err error) { 26 | return lc.buffer.Write(p) 27 | } 28 | 29 | func (lc *LogCapture) String() string { 30 | return lc.buffer.String() 31 | } 32 | 33 | // func TestHandlePfcpHeartbeatRequest(t *testing.T) { 34 | // } 35 | 36 | func TestHandlePfcpManagementRequest(t *testing.T) { 37 | re := regexp.MustCompile(`(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z)(.*)`) 38 | Convey("Test log message", t, func() { 39 | remoteAddr := &net.UDPAddr{} 40 | testPfcpReq := &pfcp.Message{} 41 | msg := pfcpUdp.NewMessage(remoteAddr, testPfcpReq) 42 | logCapture := &LogCapture{} 43 | logger.Log.SetOutput(io.MultiWriter(logCapture, logrus.StandardLogger().Out)) 44 | handler.HandlePfcpPfdManagementRequest(msg) 45 | capturedLogs := re.FindStringSubmatch(logCapture.String()) 46 | 47 | logCaptureExp := &LogCapture{} 48 | logger.Log.SetOutput(io.MultiWriter(logCaptureExp)) 49 | logger.PfcpLog.Warnf("PFCP PFD Management Request handling is not implemented") 50 | capturedLogsExp := re.FindStringSubmatch(logCaptureExp.String()) 51 | So(capturedLogs[2], ShouldEqual, capturedLogsExp[2]) 52 | }) 53 | } 54 | 55 | func TestHandlePfcpAssociationSetupRequest(t *testing.T) { 56 | re := regexp.MustCompile(`(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z)(.*)`) 57 | re2 := regexp.MustCompile(`(.*)\n(.*)`) 58 | Convey("Test if NodeID is Nil", t, func() { 59 | remoteAddr := &net.UDPAddr{ 60 | IP: net.ParseIP("192.168.1.1"), 61 | Port: 12345, 62 | } 63 | 64 | testPfcpReq := &pfcp.Message{ 65 | Header: pfcp.Header{ 66 | Version: 1, 67 | MP: 0, 68 | S: 0, 69 | MessageType: pfcp.PFCP_ASSOCIATION_SETUP_REQUEST, 70 | MessageLength: 9, 71 | SEID: 0, 72 | SequenceNumber: 1, 73 | MessagePriority: 0, 74 | }, 75 | Body: pfcp.PFCPAssociationSetupRequest{ 76 | NodeID: nil, 77 | }, 78 | } 79 | 80 | logCapture := &LogCapture{} 81 | logger.Log.SetOutput(io.MultiWriter(logCapture, logrus.StandardLogger().Out)) 82 | 83 | msg := pfcpUdp.NewMessage(remoteAddr, testPfcpReq) 84 | handler.HandlePfcpAssociationSetupRequest(msg) 85 | capturedLogs := re.FindStringSubmatch(logCapture.String()) 86 | 87 | logCaptureExp := &LogCapture{} 88 | logger.Log.SetOutput(io.MultiWriter(logCaptureExp)) 89 | logger.PfcpLog.Errorln("pfcp association needs NodeID") 90 | logger.PfcpLog.Infof("Handle PFCP Association Setup Request with NodeID") 91 | ExpLogs := re.FindStringSubmatch(logCaptureExp.String()) 92 | fmt.Println(ExpLogs) 93 | if len(capturedLogs) <= 2 || len(ExpLogs) <= 2 { 94 | t.Errorf("The extracted log is not as expected.") 95 | } 96 | So(capturedLogs[2], ShouldEqual, ExpLogs[2]) 97 | }) 98 | Convey("Test if NodeID is NotNil, upf is Nil", t, func() { 99 | remoteAddr := &net.UDPAddr{ 100 | IP: net.ParseIP("192.168.1.1"), 101 | Port: 12345, 102 | } 103 | 104 | testPfcpReq := &pfcp.Message{ 105 | Header: pfcp.Header{ 106 | Version: 1, 107 | MP: 0, 108 | S: 0, 109 | MessageType: pfcp.PFCP_ASSOCIATION_SETUP_REQUEST, 110 | MessageLength: 9, 111 | SEID: 0, 112 | SequenceNumber: 1, 113 | MessagePriority: 0, 114 | }, 115 | Body: pfcp.PFCPAssociationSetupRequest{ 116 | NodeID: &pfcpType.NodeID{ 117 | NodeIdType: pfcpType.NodeIdTypeIpv4Address, 118 | IP: net.ParseIP("192.168.1.1").To4(), 119 | }, 120 | }, 121 | } 122 | 123 | logCapture := &LogCapture{} 124 | logger.Log.SetOutput(io.MultiWriter(logCapture, logrus.StandardLogger().Out)) 125 | 126 | msg := pfcpUdp.NewMessage(remoteAddr, testPfcpReq) 127 | handler.HandlePfcpAssociationSetupRequest(msg) 128 | capturedLogs := re.FindStringSubmatch(re2.FindStringSubmatch(logCapture.String())[1]) 129 | 130 | logCaptureExp := &LogCapture{} 131 | logger.Log.SetOutput(io.MultiWriter(logCaptureExp)) 132 | logger.PfcpLog.Infof("Handle PFCP Association Setup Request with NodeID[%s]", "192.168.1.1") 133 | logger.PfcpLog.Errorf("can't find UPF[%s]", "192.168.1.1") 134 | ExpLogs := re.FindStringSubmatch(re2.FindStringSubmatch(logCaptureExp.String())[1]) 135 | if len(capturedLogs) <= 2 || len(ExpLogs) <= 2 { 136 | t.Errorf("The extracted log is not as expected.") 137 | } 138 | So(capturedLogs[2], ShouldEqual, ExpLogs[2]) 139 | capturedLogs = re.FindStringSubmatch(re2.FindStringSubmatch(logCapture.String())[2]) 140 | ExpLogs = re.FindStringSubmatch(re2.FindStringSubmatch(logCaptureExp.String())[2]) 141 | So(capturedLogs[2], ShouldEqual, ExpLogs[2]) 142 | }) 143 | } 144 | 145 | func TestHandlePfcpAssociationUpdateRequest(t *testing.T) { 146 | re := regexp.MustCompile(`(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z)(.*)`) 147 | Convey("Test logger message", t, func() { 148 | remoteAddr := &net.UDPAddr{} 149 | testPfcpReq := &pfcp.Message{} 150 | msg := pfcpUdp.NewMessage(remoteAddr, testPfcpReq) 151 | logCapture := &LogCapture{} 152 | logger.Log.SetOutput(io.MultiWriter(logCapture, logrus.StandardLogger().Out)) 153 | handler.HandlePfcpAssociationUpdateRequest(msg) 154 | capturedLogs := re.FindStringSubmatch(logCapture.String()) 155 | 156 | logCaptureExp := &LogCapture{} 157 | logger.Log.SetOutput(io.MultiWriter(logCaptureExp)) 158 | logger.PfcpLog.Warnf("PFCP Association Update Request handling is not implemented") 159 | capturedLogsExp := re.FindStringSubmatch(logCaptureExp.String()) 160 | So(capturedLogs[2], ShouldEqual, capturedLogsExp[2]) 161 | }) 162 | } 163 | 164 | // func TestHandlePfcpAssociationReleaseRequest(t *testing.T) { 165 | // } 166 | -------------------------------------------------------------------------------- /internal/pfcp/message/build_test.go: -------------------------------------------------------------------------------- 1 | package message_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/free5gc/pfcp/pfcpType" 10 | "github.com/free5gc/smf/internal/context" 11 | "github.com/free5gc/smf/internal/pfcp/message" 12 | "github.com/free5gc/smf/internal/pfcp/udp" 13 | "github.com/free5gc/smf/pkg/factory" 14 | ) 15 | 16 | var testConfig = factory.Config{ 17 | Info: &factory.Info{ 18 | Version: "1.0.0", 19 | Description: "SMF procdeure test configuration", 20 | }, 21 | Configuration: &factory.Configuration{ 22 | Sbi: &factory.Sbi{ 23 | Scheme: "http", 24 | RegisterIPv4: "127.0.0.1", 25 | BindingIPv4: "127.0.0.1", 26 | Port: 8000, 27 | }, 28 | PFCP: &factory.PFCP{ 29 | NodeID: "10.4.0.1", 30 | }, 31 | }, 32 | } 33 | 34 | var testNodeID = &pfcpType.NodeID{ 35 | NodeIdType: pfcpType.NodeIdTypeIpv4Address, 36 | IP: net.ParseIP("10.4.0.1").To4(), 37 | } 38 | 39 | func initSmfContext() { 40 | context.InitSmfContext(&testConfig) 41 | } 42 | 43 | func initRuleList() ([]*context.PDR, []*context.FAR, []*context.BAR, 44 | []*context.QER, []*context.URR, 45 | ) { 46 | testPDR := &context.PDR{ 47 | PDRID: uint16(1), 48 | State: context.RULE_INITIAL, 49 | OuterHeaderRemoval: &pfcpType.OuterHeaderRemoval{ 50 | OuterHeaderRemovalDescription: (1), 51 | }, 52 | FAR: &context.FAR{}, 53 | URR: []*context.URR{}, 54 | QER: []*context.QER{}, 55 | } 56 | 57 | testFAR := &context.FAR{ 58 | FARID: uint32(123), 59 | // State Can be RULE_INITIAL or RULE_UPDATE or RULE_REMOVE 60 | State: context.RULE_INITIAL, 61 | ApplyAction: pfcpType.ApplyAction{ 62 | Forw: true, 63 | }, 64 | ForwardingParameters: &context.ForwardingParameters{}, 65 | BAR: &context.BAR{}, 66 | } 67 | 68 | testBAR := &context.BAR{ 69 | BARID: uint8(124), 70 | // State Can be RULE_INITIAL or RULE_UPDATE or RULE_REMOVE 71 | State: context.RULE_INITIAL, 72 | } 73 | 74 | testQER := &context.QER{ 75 | QERID: uint32(123), 76 | // State Can be RULE_INITIAL or RULE_UPDATE or RULE_REMOVE 77 | State: context.RULE_INITIAL, 78 | } 79 | 80 | testURR := &context.URR{ 81 | URRID: uint32(123), 82 | // State Can be RULE_INITIAL or RULE_UPDATE or RULE_REMOVE 83 | State: context.RULE_INITIAL, 84 | } 85 | pdrList := make([]*context.PDR, 0) 86 | farList := make([]*context.FAR, 0) 87 | barList := make([]*context.BAR, 0) 88 | qerList := make([]*context.QER, 0) 89 | urrList := make([]*context.URR, 0) 90 | pdrList = append(pdrList, testPDR) 91 | farList = append(farList, testFAR) 92 | barList = append(barList, testBAR) 93 | qerList = append(qerList, testQER) 94 | urrList = append(urrList, testURR) 95 | return pdrList, farList, barList, qerList, urrList 96 | } 97 | 98 | func TestBuildPfcpAssociationSetupRequest(t *testing.T) { 99 | emptyReq, err := message.BuildPfcpAssociationSetupRequest() 100 | if err != nil { 101 | t.Errorf("TestBuildPfcpAssociationSetupRequest failed: %v", err) 102 | } 103 | 104 | // BuildPfcpAssociationSetupRequest buila a empty template of pfcp.PFCPAssociationSetupRequest 105 | assert.Equal(t, uint8(0), emptyReq.NodeID.NodeIdType) 106 | assert.Equal(t, net.IP(nil), emptyReq.NodeID.IP) 107 | assert.Equal(t, "", emptyReq.NodeID.FQDN) 108 | 109 | assert.Equal(t, 110 | udp.ServerStartTime, 111 | emptyReq.RecoveryTimeStamp.RecoveryTimeStamp) 112 | assert.Nil(t, 113 | emptyReq.UPFunctionFeatures) 114 | assert.Equal(t, 115 | pfcpType.CPFunctionFeatures{SupportedFeatures: 0}, 116 | *emptyReq.CPFunctionFeatures) 117 | } 118 | 119 | func TestBuildPfcpAssociationSetupResponse(t *testing.T) { 120 | cause := pfcpType.Cause{CauseValue: pfcpType.CauseRequestAccepted} 121 | rsp, err := message.BuildPfcpAssociationSetupResponse(cause) 122 | if err != nil { 123 | t.Errorf("TestBuildPfcpAssociationSetupResponse failed: %v", err) 124 | } 125 | 126 | assert.Equal(t, uint8(0), rsp.NodeID.NodeIdType) 127 | assert.Equal(t, cause, *rsp.Cause) 128 | 129 | assert.Nil(t, 130 | rsp.UPFunctionFeatures) 131 | assert.Equal(t, 132 | pfcpType.CPFunctionFeatures{SupportedFeatures: 0}, 133 | *rsp.CPFunctionFeatures) 134 | } 135 | 136 | func TestBuildPfcpAssociationReleaseRequest(t *testing.T) { 137 | emptyReq, err := message.BuildPfcpAssociationReleaseRequest() 138 | if err != nil { 139 | t.Errorf("TestBuildPfcpAssociationReleaseRequest failed: %v", err) 140 | } 141 | 142 | assert.Equal(t, uint8(0), emptyReq.NodeID.NodeIdType) 143 | } 144 | 145 | func TestBuildPfcpAssociationReleaseResponse(t *testing.T) { 146 | cause := pfcpType.Cause{CauseValue: pfcpType.CauseRequestAccepted} 147 | rsp, err := message.BuildPfcpAssociationReleaseResponse(cause) 148 | if err != nil { 149 | t.Errorf("TestBuildPfcpAssociationReleaseResponse failed: %v", err) 150 | } 151 | 152 | assert.Equal(t, uint8(0), rsp.NodeID.NodeIdType) 153 | assert.Equal(t, cause, *rsp.Cause) 154 | } 155 | 156 | func TestBuildPfcpSessionEstablishmentRequest(t *testing.T) { 157 | initSmfContext() 158 | smctx := context.NewSMContext("imsi-208930000000001", 10) 159 | pdrList, farList, barList, qerList, urrList := initRuleList() 160 | smctx.PFCPContext["10.4.0.1"] = &context.PFCPSessionContext{} 161 | 162 | req, err := message.BuildPfcpSessionEstablishmentRequest( 163 | *testNodeID, "10.4.0.1", smctx, pdrList, farList, barList, qerList, urrList) 164 | if err != nil { 165 | t.Errorf("TestBuildPfcpSessionEstablishmentRequest failed: %v", err) 166 | } 167 | // assert.Equal(t, uint8(0), req.NodeID.NodeIdType) 168 | assert.Equal(t, testNodeID, req.NodeID) 169 | assert.Equal(t, &pfcpType.PDNType{PdnType: pfcpType.PDNTypeIpv4}, req.PDNType) 170 | assert.Equal(t, len(req.CreatePDR), 1) 171 | assert.Equal(t, len(req.CreateFAR), 1) 172 | assert.Equal(t, len(req.CreateBAR), 1) 173 | assert.Equal(t, len(req.CreateQER), 1) 174 | assert.Equal(t, len(req.CreateURR), 1) 175 | assert.Equal(t, pdrList[0].State, context.RULE_CREATE) 176 | assert.Equal(t, farList[0].State, context.RULE_CREATE) 177 | assert.Equal(t, barList[0].State, context.RULE_CREATE) 178 | assert.Equal(t, qerList[0].State, context.RULE_CREATE) 179 | assert.Equal(t, urrList[0].State, context.RULE_CREATE) 180 | 181 | req2, err2 := message.BuildPfcpSessionEstablishmentRequest( 182 | *testNodeID, "10.4.0.1", smctx, nil, nil, nil, nil, nil) 183 | if err2 != nil { 184 | t.Errorf("TestBuildPfcpSessionEstablishmentRequest failed: %v", err2) 185 | } 186 | assert.NotEqual(t, req2, req) 187 | assert.Equal(t, len(req2.CreatePDR), 0) 188 | assert.Equal(t, len(req2.CreateFAR), 0) 189 | assert.Equal(t, len(req2.CreateBAR), 0) 190 | assert.Equal(t, len(req2.CreateQER), 0) 191 | assert.Equal(t, len(req2.CreateURR), 0) 192 | } 193 | 194 | // hsien 195 | func TestBuildPfcpSessionEstablishmentResponse(t *testing.T) { 196 | initSmfContext() 197 | rsp, err := message.BuildPfcpSessionEstablishmentResponse() 198 | if err != nil { 199 | t.Errorf("TestBuildPfcpSessionEstablishmentResponse failed: %v", err) 200 | } 201 | assert.Equal(t, rsp.NodeID, testNodeID) 202 | assert.Equal(t, uint8(0), rsp.NodeID.NodeIdType) 203 | assert.Equal(t, pfcpType.CauseRequestAccepted, rsp.Cause.CauseValue) 204 | assert.NotNil(t, rsp.UPFSEID) 205 | assert.NotNil(t, rsp.CreatedPDR) 206 | } 207 | 208 | func TestBuildPfcpSessionModificationRequest(t *testing.T) { 209 | initSmfContext() 210 | smctx := context.NewSMContext("imsi-208930000000001", 10) 211 | pdrList, farList, barList, qerList, urrList := initRuleList() 212 | smctx.PFCPContext["10.4.0.1"] = &context.PFCPSessionContext{} 213 | 214 | req, err := message.BuildPfcpSessionModificationRequest( 215 | *testNodeID, "10.4.0.1", smctx, pdrList, farList, barList, qerList, urrList) 216 | if err != nil { 217 | t.Errorf("TestBuildPfcpSessionModificationRequest failed: %v", err) 218 | } 219 | 220 | assert.Equal(t, context.RULE_CREATE, pdrList[0].State) 221 | assert.Equal(t, context.RULE_CREATE, farList[0].State) 222 | assert.Equal(t, context.RULE_INITIAL, barList[0].State) 223 | assert.Equal(t, context.RULE_CREATE, qerList[0].State) 224 | assert.Equal(t, context.RULE_CREATE, urrList[0].State) 225 | 226 | assert.Equal(t, len(req.CreatePDR), 1) 227 | assert.Equal(t, len(req.CreateFAR), 1) 228 | assert.Equal(t, len(req.CreateBAR), 1) 229 | assert.Equal(t, len(req.CreateQER), 1) 230 | assert.Equal(t, len(req.CreateURR), 1) 231 | } 232 | 233 | func TestBuildPfcpSessionModificationResponse(t *testing.T) { 234 | initSmfContext() 235 | rsp, err := message.BuildPfcpSessionEstablishmentResponse() 236 | if err != nil { 237 | t.Errorf("BuildPfcpSessionModificationResponse failed: %v", err) 238 | } 239 | assert.Equal(t, rsp.NodeID, testNodeID) 240 | assert.Equal(t, pfcpType.CauseRequestAccepted, rsp.Cause.CauseValue) 241 | assert.NotNil(t, rsp.OffendingIE) 242 | assert.NotNil(t, rsp.CreatedPDR) 243 | } 244 | 245 | func TestBuildPfcpSessionDeletionResponse(t *testing.T) { 246 | _, err := message.BuildPfcpSessionDeletionResponse() 247 | if err != nil { 248 | t.Errorf("TestBuildPfcpSessionDeletionResponse failed: %v", err) 249 | } 250 | } 251 | 252 | func TestBuildPfcpSessionReportResponse(t *testing.T) { 253 | cause := pfcpType.Cause{CauseValue: pfcpType.CauseRequestAccepted} 254 | rsp, err := message.BuildPfcpSessionReportResponse(cause) 255 | if err != nil { 256 | t.Errorf("TestBuildPfcpSessionReportResponse failed: %v", err) 257 | } 258 | assert.Equal(t, cause, *rsp.Cause) 259 | } 260 | 261 | func TestBuildPfcpHeartbeatRequest(t *testing.T) { 262 | rsq, err := message.BuildPfcpHeartbeatRequest() 263 | if err != nil { 264 | t.Errorf("TestBuildPfcpHeartbeatRequest failed: %v", err) 265 | } 266 | 267 | assert.Equal(t, udp.ServerStartTime, rsq.RecoveryTimeStamp.RecoveryTimeStamp) 268 | } 269 | -------------------------------------------------------------------------------- /internal/pfcp/message/send_test.go: -------------------------------------------------------------------------------- 1 | package message_test 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | smf_context "github.com/free5gc/smf/internal/context" 12 | smf_pfcp "github.com/free5gc/smf/internal/pfcp" 13 | "github.com/free5gc/smf/internal/pfcp/message" 14 | "github.com/free5gc/smf/internal/pfcp/udp" 15 | ) 16 | 17 | func TestSendPfcpAssociationSetupRequest(t *testing.T) { 18 | } 19 | 20 | func TestSendPfcpSessionEstablishmentResponse(t *testing.T) { 21 | } 22 | 23 | func TestSendPfcpSessionEstablishmentRequest(t *testing.T) { 24 | } 25 | 26 | func TestSendHeartbeatResponse(t *testing.T) { 27 | smfContext := smf_context.GetSelf() 28 | smfContext.PfcpContext, smfContext.PfcpCancelFunc = context.WithCancel(context.Background()) 29 | udp.Run(smf_pfcp.Dispatch) 30 | 31 | udp.ServerStartTime = time.Now() 32 | var seq uint32 = 1 33 | addr := &net.UDPAddr{ 34 | IP: net.ParseIP("127.0.0.1"), 35 | Port: 7001, 36 | } 37 | message.SendHeartbeatResponse(addr, seq) 38 | 39 | err := udp.ClosePfcp() 40 | require.NoError(t, err) 41 | } 42 | -------------------------------------------------------------------------------- /internal/pfcp/reliable_pfcp_request_test.go: -------------------------------------------------------------------------------- 1 | package pfcp_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // var testAddr *net.UDPAddr 8 | 9 | // Adjust waiting time in millisecond if PFCP packets are not captured 10 | // var testWaitingTime int = 500 11 | 12 | func init() { 13 | // smfContext := context.GetSelf() 14 | 15 | // smfContext.CPNodeID.NodeIdType = 0 16 | // smfContext.CPNodeID.NodeIdValue = net.ParseIP("127.0.0.2").To4() 17 | 18 | // udp.Run() 19 | 20 | // testAddr = &net.UDPAddr{ 21 | // IP: net.ParseIP("127.0.0.2"), 22 | // Port: pfcpUdp.PFCP_PORT, 23 | // } 24 | } 25 | 26 | func TestReliablePFCPResponseDelivery(t *testing.T) { 27 | // conn, err := net.DialUDP("udp", nil, testAddr) 28 | 29 | // if err != nil { 30 | // fmt.Println(err.Error()) 31 | // } 32 | // pfcpMsg, err := message.BuildPfcpAssociationReleaseRequest() 33 | 34 | // if err != nil { 35 | // fmt.Println(err.Error()) 36 | // } 37 | // msg := pfcp.Message{ 38 | // Header: pfcp.Header{ 39 | // Version: pfcp.PfcpVersion, 40 | // MP: 0, 41 | // S: pfcp.SEID_NOT_PRESENT, 42 | // MessageType: pfcp.PFCP_ASSOCIATION_RELEASE_REQUEST, 43 | // SequenceNumber: 1, 44 | // }, 45 | // Body: pfcpMsg, 46 | // } 47 | 48 | // buf, err := msg.Marshal() 49 | // if err != nil { 50 | // fmt.Println(err.Error()) 51 | // } 52 | // _, err = conn.Write(buf) 53 | // if err != nil { 54 | // fmt.Println(err.Error()) 55 | // } 56 | 57 | // time.Sleep(2 * time.Second) 58 | 59 | // _, err = conn.Write(buf) 60 | // if err != nil { 61 | // fmt.Println(err.Error()) 62 | // } 63 | 64 | // time.Sleep(2 * time.Second) 65 | 66 | // _, err = conn.Write(buf) 67 | // if err != nil { 68 | // fmt.Println(err.Error()) 69 | // } 70 | 71 | // time.Sleep(20 * time.Second) 72 | } 73 | -------------------------------------------------------------------------------- /internal/pfcp/udp/udp.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "runtime/debug" 7 | "strings" 8 | "time" 9 | 10 | "github.com/free5gc/pfcp" 11 | "github.com/free5gc/pfcp/pfcpUdp" 12 | smf_context "github.com/free5gc/smf/internal/context" 13 | "github.com/free5gc/smf/internal/logger" 14 | ) 15 | 16 | const MaxPfcpUdpDataSize = 1024 17 | 18 | var Server *pfcpUdp.PfcpServer 19 | 20 | var ServerStartTime time.Time 21 | 22 | func Run(dispatch func(*pfcpUdp.Message)) { 23 | defer func() { 24 | if p := recover(); p != nil { 25 | // Print stack for panic to log. Fatalf() will let program exit. 26 | logger.PfcpLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 27 | } 28 | }() 29 | 30 | smfContext := smf_context.GetSelf() 31 | 32 | serverIP := smfContext.ListenIP().To4() 33 | Server = pfcpUdp.NewPfcpServer(serverIP.String()) 34 | 35 | err := Server.Listen() 36 | if err != nil { 37 | logger.PfcpLog.Errorf("Failed to listen: %v", err) 38 | } 39 | 40 | logger.PfcpLog.Infof("Listen on %s", Server.Conn.LocalAddr().String()) 41 | 42 | go func(p *pfcpUdp.PfcpServer) { 43 | defer func() { 44 | if p := recover(); p != nil { 45 | // Print stack for panic to log. Fatalf() will let program exit. 46 | logger.PfcpLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 47 | } 48 | }() 49 | 50 | for { 51 | msg, errReadFrom := p.ReadFrom() 52 | if errReadFrom != nil { 53 | if errReadFrom == pfcpUdp.ErrReceivedResentRequest { 54 | logger.PfcpLog.Infoln(errReadFrom) 55 | } else if strings.Contains(errReadFrom.Error(), "use of closed network connection") { 56 | continue 57 | } else { 58 | logger.PfcpLog.Warnf("Read PFCP error: %v, msg: [%v]", errReadFrom, msg) 59 | select { 60 | case <-smfContext.PfcpContext.Done(): 61 | // PFCP is closing 62 | return 63 | default: 64 | continue 65 | } 66 | } 67 | continue 68 | } 69 | 70 | if msg.PfcpMessage.IsRequest() { 71 | go dispatch(msg) 72 | } 73 | } 74 | }(Server) 75 | 76 | ServerStartTime = time.Now() 77 | 78 | logger.PfcpLog.Infof("Pfcp running... [%v]", ServerStartTime) 79 | } 80 | 81 | func SendPfcpResponse(sndMsg *pfcp.Message, addr *net.UDPAddr) { 82 | Server.WriteResponseTo(sndMsg, addr) 83 | } 84 | 85 | func SendPfcpRequest(sndMsg *pfcp.Message, addr *net.UDPAddr) (rsvMsg *pfcpUdp.Message, err error) { 86 | if addr.IP.Equal(net.IPv4zero) { 87 | return nil, errors.New("no destination IP address is specified") 88 | } 89 | return Server.WriteRequestTo(sndMsg, addr) 90 | } 91 | 92 | func ClosePfcp() error { 93 | smf_context.GetSelf().PfcpCancelFunc() 94 | 95 | closeErr := Server.Close() 96 | if closeErr != nil { 97 | logger.PfcpLog.Errorf("Pfcp close err: %+v", closeErr) 98 | } else { 99 | logger.PfcpLog.Infof("Pfcp server closed") 100 | } 101 | return closeErr 102 | } 103 | -------------------------------------------------------------------------------- /internal/pfcp/udp/udp_test.go: -------------------------------------------------------------------------------- 1 | package udp_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/free5gc/pfcp" 11 | "github.com/free5gc/pfcp/pfcpType" 12 | "github.com/free5gc/pfcp/pfcpUdp" 13 | "github.com/free5gc/smf/internal/context" 14 | smf_pfcp "github.com/free5gc/smf/internal/pfcp" 15 | "github.com/free5gc/smf/internal/pfcp/udp" 16 | ) 17 | 18 | const testPfcpClientPort = 12345 19 | 20 | func TestRun(t *testing.T) { 21 | // Set SMF Node ID 22 | 23 | context.GetSelf().CPNodeID = pfcpType.NodeID{ 24 | NodeIdType: pfcpType.NodeIdTypeIpv4Address, 25 | IP: net.ParseIP("127.0.0.1").To4(), 26 | } 27 | context.GetSelf().ExternalAddr = "127.0.0.1" 28 | context.GetSelf().ListenAddr = "127.0.0.1" 29 | 30 | udp.Run(smf_pfcp.Dispatch) 31 | 32 | testPfcpReq := pfcp.Message{ 33 | Header: pfcp.Header{ 34 | Version: 1, 35 | MP: 0, 36 | S: 0, 37 | MessageType: pfcp.PFCP_ASSOCIATION_SETUP_REQUEST, 38 | MessageLength: 9, 39 | SEID: 0, 40 | SequenceNumber: 1, 41 | MessagePriority: 0, 42 | }, 43 | Body: pfcp.PFCPAssociationSetupRequest{ 44 | NodeID: &pfcpType.NodeID{ 45 | NodeIdType: 0, 46 | IP: net.ParseIP("192.168.1.1").To4(), 47 | }, 48 | }, 49 | } 50 | 51 | srcAddr := &net.UDPAddr{ 52 | IP: net.ParseIP("127.0.0.1"), 53 | Port: testPfcpClientPort, 54 | } 55 | dstAddr := &net.UDPAddr{ 56 | IP: net.ParseIP("127.0.0.1"), 57 | Port: pfcpUdp.PFCP_PORT, 58 | } 59 | 60 | err := pfcpUdp.SendPfcpMessage(testPfcpReq, srcAddr, dstAddr) 61 | require.Nil(t, err) 62 | 63 | err = udp.Server.Close() 64 | require.NoError(t, err) 65 | 66 | time.Sleep(300 * time.Millisecond) 67 | } 68 | -------------------------------------------------------------------------------- /internal/sbi/api_callback.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/free5gc/openapi" 10 | "github.com/free5gc/openapi/models" 11 | "github.com/free5gc/smf/internal/logger" 12 | ) 13 | 14 | func (s *Server) getCallbackRoutes() []Route { 15 | return []Route{ 16 | { 17 | Name: "SmPolicyUpdateNotification", 18 | Method: http.MethodPost, 19 | Pattern: "/sm-policies/:smContextRef/update", 20 | APIFunc: s.HTTPSmPolicyUpdateNotification, 21 | }, 22 | { 23 | Name: "SmPolicyControlTerminationRequestNotification", 24 | Method: http.MethodPost, 25 | Pattern: "/sm-policies/:smContextRef/terminate", 26 | APIFunc: s.SmPolicyControlTerminationRequestNotification, 27 | }, 28 | { 29 | Name: "ChargingNotification", 30 | Method: http.MethodPost, 31 | Pattern: "/:notifyUri", 32 | APIFunc: s.HTTPChargingNotification, 33 | }, 34 | } 35 | } 36 | 37 | // SubscriptionsPost - 38 | func (s *Server) HTTPSmPolicyUpdateNotification(c *gin.Context) { 39 | var request models.SmPolicyNotification 40 | 41 | reqBody, err := c.GetRawData() 42 | if err != nil { 43 | logger.PduSessLog.Errorln("GetRawData failed") 44 | } 45 | 46 | err = openapi.Deserialize(&request, reqBody, c.ContentType()) 47 | if err != nil { 48 | logger.PduSessLog.Errorln("Deserialize request failed") 49 | } 50 | 51 | smContextRef := c.Params.ByName("smContextRef") 52 | s.Processor().HandleSMPolicyUpdateNotify(c, request, smContextRef) 53 | } 54 | 55 | func (s *Server) SmPolicyControlTerminationRequestNotification(c *gin.Context) { 56 | c.JSON(http.StatusNotImplemented, gin.H{}) 57 | } 58 | 59 | func (s *Server) HTTPChargingNotification(c *gin.Context) { 60 | var req models.ChargingNotifyRequest 61 | 62 | requestBody, err := c.GetRawData() 63 | if err != nil { 64 | logger.PduSessLog.Errorln("GetRawData failed") 65 | } 66 | 67 | err = openapi.Deserialize(&req, requestBody, APPLICATION_JSON) 68 | if err != nil { 69 | logger.PduSessLog.Errorln("Deserialize request failed") 70 | } 71 | 72 | smContextRef := strings.Split(c.Params.ByName("notifyUri"), "_")[1] 73 | 74 | s.Processor().HandleChargingNotification(c, req, smContextRef) 75 | } 76 | -------------------------------------------------------------------------------- /internal/sbi/api_eventexposure.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Nsmf_EventExposure 3 | * 4 | * Session Management Event Exposure Service API 5 | * 6 | * API version: 1.0.0 7 | * Generated by: OpenAPI Generator (https://openapi-generator.tech) 8 | */ 9 | 10 | package sbi 11 | 12 | import ( 13 | "net/http" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | func (s *Server) getEventExposureRoutes() []Route { 19 | return []Route{ 20 | { 21 | Name: "Index", 22 | Method: http.MethodGet, 23 | Pattern: "/", 24 | APIFunc: func(c *gin.Context) { 25 | c.JSON(http.StatusOK, gin.H{"status": "Service Available"}) 26 | }, 27 | }, 28 | { 29 | Name: "CreateIndividualSubcription", 30 | Method: http.MethodPost, 31 | Pattern: "/subscriptions", 32 | APIFunc: s.HTTPCreateIndividualSubcription, 33 | }, 34 | { 35 | Name: "DeleteIndividualSubcription", 36 | Method: http.MethodDelete, 37 | Pattern: "/subscriptions/:subId", 38 | APIFunc: s.HTTPDeleteIndividualSubcription, 39 | }, 40 | { 41 | Name: "GetIndividualSubcription", 42 | Method: http.MethodGet, 43 | Pattern: "/subscriptions/:subId", 44 | APIFunc: s.HTTPGetIndividualSubcription, 45 | }, 46 | { 47 | Name: "ReplaceIndividualSubcription", 48 | Method: http.MethodPut, 49 | Pattern: "/subscriptions/:subId", 50 | APIFunc: s.HTTPReplaceIndividualSubcription, 51 | }, 52 | } 53 | } 54 | 55 | // SubscriptionsPost - 56 | func (s *Server) HTTPCreateIndividualSubcription(c *gin.Context) { 57 | c.JSON(http.StatusNotImplemented, gin.H{}) 58 | } 59 | 60 | // SubscriptionsSubIdDelete - 61 | func (s *Server) HTTPDeleteIndividualSubcription(c *gin.Context) { 62 | c.JSON(http.StatusNotImplemented, gin.H{}) 63 | } 64 | 65 | // SubscriptionsSubIdGet - 66 | func (s *Server) HTTPGetIndividualSubcription(c *gin.Context) { 67 | c.JSON(http.StatusNotImplemented, gin.H{}) 68 | } 69 | 70 | // SubscriptionsSubIdPut - 71 | func (s *Server) HTTPReplaceIndividualSubcription(c *gin.Context) { 72 | c.JSON(http.StatusNotImplemented, gin.H{}) 73 | } 74 | -------------------------------------------------------------------------------- /internal/sbi/api_oam.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (s *Server) getOAMRoutes() []Route { 10 | return []Route{ 11 | { 12 | Name: "Index", 13 | Method: http.MethodGet, 14 | Pattern: "/", 15 | APIFunc: func(c *gin.Context) { 16 | c.JSON(http.StatusOK, gin.H{"status": "Service Available"}) 17 | }, 18 | }, 19 | { 20 | Name: "Get UE PDU Session Info", 21 | Method: http.MethodGet, 22 | Pattern: "/ue-pdu-session-info/:smContextRef", 23 | APIFunc: s.HTTPGetUEPDUSessionInfo, 24 | }, 25 | { 26 | Name: "Get SMF Userplane Information", 27 | Method: http.MethodGet, 28 | Pattern: "/user-plane-info/", 29 | APIFunc: s.HTTPGetSMFUserPlaneInfo, 30 | }, 31 | } 32 | } 33 | 34 | func (s *Server) HTTPGetUEPDUSessionInfo(c *gin.Context) { 35 | smContextRef := c.Params.ByName("smContextRef") 36 | 37 | s.Processor().HandleOAMGetUEPDUSessionInfo(c, smContextRef) 38 | } 39 | 40 | func (s *Server) HTTPGetSMFUserPlaneInfo(c *gin.Context) { 41 | s.Processor().HandleGetSMFUserPlaneInfo(c) 42 | } 43 | -------------------------------------------------------------------------------- /internal/sbi/api_pdusession.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/free5gc/openapi" 11 | "github.com/free5gc/openapi/models" 12 | "github.com/free5gc/smf/internal/logger" 13 | ) 14 | 15 | func (s *Server) getPDUSessionRoutes() []Route { 16 | return []Route{ 17 | { 18 | Name: "Index", 19 | Method: http.MethodGet, 20 | Pattern: "/", 21 | APIFunc: func(c *gin.Context) { 22 | c.JSON(http.StatusOK, gin.H{"status": "Service Available"}) 23 | }, 24 | }, 25 | { 26 | Name: "PostSmContexts", 27 | Method: http.MethodPost, 28 | Pattern: "/sm-contexts", 29 | APIFunc: s.HTTPPostSmContexts, 30 | }, 31 | { 32 | Name: "UpdateSmContext", 33 | Method: http.MethodPost, 34 | Pattern: "/sm-contexts/:smContextRef/modify", 35 | APIFunc: s.HTTPUpdateSmContext, 36 | }, 37 | { 38 | Name: "RetrieveSmContext", 39 | Method: http.MethodPost, 40 | Pattern: "/sm-contexts/:smContextRef/retrieve", 41 | APIFunc: s.HTTPRetrieveSmContext, 42 | }, 43 | { 44 | Name: "ReleaseSmContext", 45 | Method: http.MethodPost, 46 | Pattern: "/sm-contexts/:smContextRef/release", 47 | APIFunc: s.HTTPReleaseSmContext, 48 | }, 49 | { 50 | Name: "SendMoData", 51 | Method: http.MethodPost, 52 | Pattern: "/sm-contexts/:smContextRef/send-mo-data", 53 | APIFunc: s.HTTPSendMoData, 54 | }, 55 | { 56 | Name: "PostPduSessions", 57 | Method: http.MethodPatch, 58 | Pattern: "/pdu-sessions", 59 | APIFunc: s.HTTPPostPduSessions, 60 | }, 61 | { 62 | Name: "UpdatePduSession", 63 | Method: http.MethodPost, 64 | Pattern: "/pdu-sessions/:pduSessionRef/modify", 65 | APIFunc: s.HTTPUpdatePduSession, 66 | }, 67 | { 68 | Name: "ReleasePduSession", 69 | Method: http.MethodPost, 70 | Pattern: "/pdu-sessions/:pduSessionRef/release", 71 | APIFunc: s.HTTPReleasePduSession, 72 | }, 73 | { 74 | Name: "RetrievePduSession", 75 | Method: http.MethodPost, 76 | Pattern: "/pdu-sessions/:pduSessionRef/retrieve", 77 | APIFunc: s.HTTPRetrievePduSession, 78 | }, 79 | { 80 | Name: "TransferMoData", 81 | Method: http.MethodPost, 82 | Pattern: "/pdu-sessions/:pduSessionRef/transfer-mo-data", 83 | APIFunc: s.HTTPTransferMoData, 84 | }, 85 | } 86 | } 87 | 88 | // HTTPPostSmContexts - Create SM Context 89 | func (s *Server) HTTPPostSmContexts(c *gin.Context) { 90 | logger.PduSessLog.Info("Receive Create SM Context Request") 91 | var request models.PostSmContextsRequest 92 | 93 | request.JsonData = new(models.SmfPduSessionSmContextCreateData) 94 | 95 | contentType := strings.Split(c.GetHeader("Content-Type"), ";") 96 | var err error 97 | switch contentType[0] { 98 | case APPLICATION_JSON: 99 | err = c.ShouldBindJSON(request.JsonData) 100 | case MULTIPART_RELATED: 101 | err = c.ShouldBindWith(&request, openapi.MultipartRelatedBinding{}) 102 | } 103 | 104 | if err != nil { 105 | problemDetail := "[Request Body] " + err.Error() 106 | logger.PduSessLog.Errorln(problemDetail) 107 | c.JSON(http.StatusBadRequest, openapi.ProblemDetailsMalformedReqSyntax(problemDetail)) 108 | return 109 | } 110 | 111 | isDone := c.Done() 112 | s.Processor().HandlePDUSessionSMContextCreate(c, request, isDone) 113 | } 114 | 115 | // HTTPUpdateSmContext - Update SM Context 116 | func (s *Server) HTTPUpdateSmContext(c *gin.Context) { 117 | logger.PduSessLog.Info("Receive Update SM Context Request") 118 | var request models.UpdateSmContextRequest 119 | request.JsonData = new(models.SmfPduSessionSmContextUpdateData) 120 | 121 | contentType := strings.Split(c.GetHeader("Content-Type"), ";") 122 | var err error 123 | switch contentType[0] { 124 | case APPLICATION_JSON: 125 | err = c.ShouldBindJSON(request.JsonData) 126 | case MULTIPART_RELATED: 127 | err = c.ShouldBindWith(&request, openapi.MultipartRelatedBinding{}) 128 | } 129 | if err != nil { 130 | log.Print(err) 131 | return 132 | } 133 | 134 | smContextRef := c.Params.ByName("smContextRef") 135 | s.Processor().HandlePDUSessionSMContextUpdate(c, request, smContextRef) 136 | } 137 | 138 | // HTTPRetrieveSmContext - Retrieve SM Context 139 | func (s *Server) HTTPRetrieveSmContext(c *gin.Context) { 140 | c.JSON(http.StatusNotImplemented, gin.H{}) 141 | } 142 | 143 | // HTTPReleaseSmContext - Release SM Context 144 | func (s *Server) HTTPReleaseSmContext(c *gin.Context) { 145 | logger.PduSessLog.Info("Receive Release SM Context Request") 146 | var request models.ReleaseSmContextRequest 147 | request.JsonData = new(models.SmfPduSessionSmContextReleaseData) 148 | 149 | contentType := strings.Split(c.GetHeader("Content-Type"), ";") 150 | var err error 151 | switch contentType[0] { 152 | case APPLICATION_JSON: 153 | err = c.ShouldBindJSON(request.JsonData) 154 | case MULTIPART_RELATED: 155 | err = c.ShouldBindWith(&request, openapi.MultipartRelatedBinding{}) 156 | } 157 | if err != nil { 158 | log.Print(err) 159 | return 160 | } 161 | 162 | smContextRef := c.Params.ByName("smContextRef") 163 | s.Processor().HandlePDUSessionSMContextRelease(c, request, smContextRef) 164 | } 165 | 166 | func (s *Server) HTTPSendMoData(c *gin.Context) { 167 | c.JSON(http.StatusNotImplemented, gin.H{}) 168 | } 169 | 170 | // HTTPPostPduSessions - Create 171 | func (s *Server) HTTPPostPduSessions(c *gin.Context) { 172 | c.JSON(http.StatusNotImplemented, gin.H{}) 173 | } 174 | 175 | // HTTPUpdatePduSession - Update (initiated by V-SMF) 176 | func (s *Server) HTTPUpdatePduSession(c *gin.Context) { 177 | c.JSON(http.StatusNotImplemented, gin.H{}) 178 | } 179 | 180 | // HTTPReleasePduSession - Release 181 | func (s *Server) HTTPReleasePduSession(c *gin.Context) { 182 | c.JSON(http.StatusNotImplemented, gin.H{}) 183 | } 184 | 185 | func (s *Server) HTTPRetrievePduSession(c *gin.Context) { 186 | c.JSON(http.StatusNotImplemented, gin.H{}) 187 | } 188 | 189 | func (s *Server) HTTPTransferMoData(c *gin.Context) { 190 | c.JSON(http.StatusNotImplemented, gin.H{}) 191 | } 192 | -------------------------------------------------------------------------------- /internal/sbi/api_upi.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | smf_context "github.com/free5gc/smf/internal/context" 9 | "github.com/free5gc/smf/pkg/factory" 10 | ) 11 | 12 | func (s *Server) getUPIRoutes() []Route { 13 | return []Route{ 14 | { 15 | Name: "Index", 16 | Method: http.MethodGet, 17 | Pattern: "/", 18 | APIFunc: func(c *gin.Context) { 19 | c.JSON(http.StatusOK, gin.H{"status": "Service Available"}) 20 | }, 21 | }, 22 | { 23 | Name: "GetUpNodesLinks", 24 | Method: http.MethodGet, 25 | Pattern: "/upNodesLinks", 26 | APIFunc: s.GetUpNodesLinks, 27 | }, 28 | { 29 | Name: "AddUpNodesLinks", 30 | Method: http.MethodPost, 31 | Pattern: "/upNodesLinks", 32 | APIFunc: s.PostUpNodesLinks, 33 | }, 34 | { 35 | Name: "DeleteUpNodeLink", 36 | Method: http.MethodDelete, 37 | Pattern: "/upNodesLinks/:upNodeRef", 38 | APIFunc: s.DeleteUpNodeLink, 39 | }, 40 | } 41 | } 42 | 43 | func (s *Server) GetUpNodesLinks(c *gin.Context) { 44 | upi := smf_context.GetSelf().UserPlaneInformation 45 | upi.Mu.RLock() 46 | defer upi.Mu.RUnlock() 47 | 48 | nodes := upi.UpNodesToConfiguration() 49 | links := upi.LinksToConfiguration() 50 | 51 | json := &factory.UserPlaneInformation{ 52 | UPNodes: nodes, 53 | Links: links, 54 | } 55 | 56 | c.JSON(http.StatusOK, json) 57 | } 58 | 59 | func (s *Server) PostUpNodesLinks(c *gin.Context) { 60 | upi := smf_context.GetSelf().UserPlaneInformation 61 | upi.Mu.Lock() 62 | defer upi.Mu.Unlock() 63 | 64 | var json factory.UserPlaneInformation 65 | if err := c.ShouldBindJSON(&json); err != nil { 66 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 67 | return 68 | } 69 | 70 | upi.UpNodesFromConfiguration(&json) 71 | upi.LinksFromConfiguration(&json) 72 | 73 | for _, upf := range upi.UPFs { 74 | // only associate new ones 75 | if err := upf.UPF.IsAssociated(); err != nil { 76 | go s.Processor().ToBeAssociatedWithUPF(smf_context.GetSelf().PfcpContext, upf.UPF) 77 | } 78 | } 79 | c.JSON(http.StatusOK, gin.H{"status": "OK"}) 80 | } 81 | 82 | func (s *Server) DeleteUpNodeLink(c *gin.Context) { 83 | // current version does not allow node deletions when ulcl is enabled 84 | if smf_context.GetSelf().ULCLSupport { 85 | c.JSON(http.StatusForbidden, gin.H{}) 86 | } else { 87 | upNodeRef := c.Params.ByName("upNodeRef") 88 | upi := smf_context.GetSelf().UserPlaneInformation 89 | upi.Mu.Lock() 90 | defer upi.Mu.Unlock() 91 | if upNode, ok := upi.UPNodes[upNodeRef]; ok { 92 | if upNode.Type == smf_context.UPNODE_UPF { 93 | go s.Processor().ReleaseAllResourcesOfUPF(upNode.UPF) 94 | } 95 | upi.UpNodeDelete(upNodeRef) 96 | upNode.UPF.CancelAssociation() 97 | c.JSON(http.StatusOK, gin.H{"status": "OK"}) 98 | } else { 99 | c.JSON(http.StatusNotFound, gin.H{}) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/sbi/consumer/amf_service.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/free5gc/openapi/amf/Communication" 9 | "github.com/free5gc/openapi/models" 10 | ) 11 | 12 | type namfService struct { 13 | consumer *Consumer 14 | 15 | CommunicationMu sync.RWMutex 16 | 17 | CommunicationClients map[string]*Communication.APIClient 18 | } 19 | 20 | func (s *namfService) getCommunicationClient(uri string) *Communication.APIClient { 21 | if uri == "" { 22 | return nil 23 | } 24 | s.CommunicationMu.RLock() 25 | client, ok := s.CommunicationClients[uri] 26 | if ok { 27 | s.CommunicationMu.RUnlock() 28 | return client 29 | } 30 | 31 | configuration := Communication.NewConfiguration() 32 | configuration.SetBasePath(uri) 33 | client = Communication.NewAPIClient(configuration) 34 | 35 | s.CommunicationMu.RUnlock() 36 | s.CommunicationMu.Lock() 37 | defer s.CommunicationMu.Unlock() 38 | s.CommunicationClients[uri] = client 39 | return client 40 | } 41 | 42 | func (s *namfService) N1N2MessageTransfer( 43 | ctx context.Context, supi string, n1n2Request models.N1N2MessageTransferRequest, apiPrefix string, 44 | ) (*models.N1N2MessageTransferRspData, error) { 45 | client := s.getCommunicationClient(apiPrefix) 46 | if client == nil { 47 | return nil, fmt.Errorf("N1N2MessageTransfer client is nil: (%v)", apiPrefix) 48 | } 49 | 50 | n1n2MessageTransferRequest := &Communication.N1N2MessageTransferRequest{ 51 | UeContextId: &supi, 52 | N1N2MessageTransferRequest: &n1n2Request, 53 | } 54 | 55 | rsp, err := client.N1N2MessageCollectionCollectionApi.N1N2MessageTransfer(ctx, n1n2MessageTransferRequest) 56 | if err != nil || rsp == nil { 57 | return nil, err 58 | } 59 | 60 | return &rsp.N1N2MessageTransferRspData, err 61 | } 62 | -------------------------------------------------------------------------------- /internal/sbi/consumer/chf_service.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/free5gc/nas/nasConvert" 10 | "github.com/free5gc/openapi" 11 | "github.com/free5gc/openapi/chf/ConvergedCharging" 12 | "github.com/free5gc/openapi/models" 13 | smf_context "github.com/free5gc/smf/internal/context" 14 | "github.com/free5gc/smf/internal/logger" 15 | ) 16 | 17 | type nchfService struct { 18 | consumer *Consumer 19 | 20 | ConvergedChargingMu sync.RWMutex 21 | 22 | ConvergedChargingClients map[string]*ConvergedCharging.APIClient 23 | } 24 | 25 | func (s *nchfService) getConvergedChargingClient(uri string) *ConvergedCharging.APIClient { 26 | if uri == "" { 27 | return nil 28 | } 29 | s.ConvergedChargingMu.RLock() 30 | client, ok := s.ConvergedChargingClients[uri] 31 | if ok { 32 | s.ConvergedChargingMu.RUnlock() 33 | return client 34 | } 35 | 36 | configuration := ConvergedCharging.NewConfiguration() 37 | configuration.SetBasePath(uri) 38 | client = ConvergedCharging.NewAPIClient(configuration) 39 | 40 | s.ConvergedChargingMu.RUnlock() 41 | s.ConvergedChargingMu.Lock() 42 | defer s.ConvergedChargingMu.Unlock() 43 | s.ConvergedChargingClients[uri] = client 44 | return client 45 | } 46 | 47 | func (s *nchfService) buildConvergedChargingRequest(smContext *smf_context.SMContext, 48 | multipleUnitUsage []models.ChfConvergedChargingMultipleUnitUsage, 49 | ) *models.ChfConvergedChargingChargingDataRequest { 50 | var triggers []models.ChfConvergedChargingTrigger 51 | 52 | smfContext := s.consumer.Context() 53 | date := time.Now() 54 | 55 | for _, unitUsage := range multipleUnitUsage { 56 | for _, usedUnit := range unitUsage.UsedUnitContainer { 57 | triggers = append(triggers, usedUnit.Triggers...) 58 | } 59 | } 60 | 61 | req := &models.ChfConvergedChargingChargingDataRequest{ 62 | ChargingId: smContext.ChargingID, 63 | SubscriberIdentifier: smContext.Supi, 64 | NfConsumerIdentification: &models.ChfConvergedChargingNfIdentification{ 65 | NodeFunctionality: models.ChfConvergedChargingNodeFunctionality_SMF, 66 | NFName: smfContext.Name, 67 | // not sure if NFIPv4Address is RegisterIPv4 or BindingIPv4 68 | NFIPv4Address: smfContext.RegisterIPv4, 69 | }, 70 | InvocationTimeStamp: &date, 71 | Triggers: triggers, 72 | PDUSessionChargingInformation: &models.ChfConvergedChargingPduSessionChargingInformation{ 73 | ChargingId: smContext.ChargingID, 74 | UserInformation: &models.ChfConvergedChargingUserInformation{ 75 | ServedGPSI: smContext.Gpsi, 76 | ServedPEI: smContext.Pei, 77 | }, 78 | PduSessionInformation: &models.ChfConvergedChargingPduSessionInformation{ 79 | PduSessionID: smContext.PDUSessionID, 80 | NetworkSlicingInfo: &models.NetworkSlicingInfo{ 81 | SNSSAI: smContext.SNssai, 82 | }, 83 | 84 | PduType: nasConvert.PDUSessionTypeToModels(smContext.SelectedPDUSessionType), 85 | ServingNetworkFunctionID: &models.ChfConvergedChargingServingNetworkFunctionId{ 86 | ServingNetworkFunctionInformation: &models.ChfConvergedChargingNfIdentification{ 87 | NodeFunctionality: models.ChfConvergedChargingNodeFunctionality_AMF, 88 | }, 89 | }, 90 | DnnId: smContext.Dnn, 91 | }, 92 | }, 93 | NotifyUri: fmt.Sprintf("%s://%s:%d/nsmf-callback/notify_%s", 94 | smfContext.URIScheme, 95 | smfContext.RegisterIPv4, 96 | smfContext.SBIPort, 97 | smContext.Ref, 98 | ), 99 | MultipleUnitUsage: multipleUnitUsage, 100 | } 101 | 102 | return req 103 | } 104 | 105 | func (s *nchfService) SendConvergedChargingRequest( 106 | smContext *smf_context.SMContext, 107 | requestType smf_context.RequestType, 108 | multipleUnitUsage []models.ChfConvergedChargingMultipleUnitUsage, 109 | ) ( 110 | *models.ChfConvergedChargingChargingDataResponse, *models.ProblemDetails, error, 111 | ) { 112 | logger.ChargingLog.Info("Handle SendConvergedChargingRequest") 113 | 114 | req := s.buildConvergedChargingRequest(smContext, multipleUnitUsage) 115 | 116 | ctx, pd, err := smf_context.GetSelf(). 117 | GetTokenCtx(models.ServiceName_NCHF_CONVERGEDCHARGING, models.NrfNfManagementNfType_CHF) 118 | if err != nil { 119 | return nil, pd, err 120 | } 121 | 122 | if smContext.SelectedCHFProfile.NfServices == nil { 123 | errMsg := "no CHF found" 124 | return nil, openapi.ProblemDetailsDataNotFound(errMsg), fmt.Errorf(errMsg) 125 | } 126 | 127 | var client *ConvergedCharging.APIClient 128 | // Create Converged Charging Client for this SM Context 129 | for _, service := range smContext.SelectedCHFProfile.NfServices { 130 | if service.ServiceName == models.ServiceName_NCHF_CONVERGEDCHARGING { 131 | client = s.getConvergedChargingClient(service.ApiPrefix) 132 | } 133 | } 134 | if client == nil { 135 | errMsg := "no CONVERGEDCHARGING-CHF found" 136 | return nil, openapi.ProblemDetailsDataNotFound(errMsg), fmt.Errorf(errMsg) 137 | } 138 | 139 | // select the appropriate converged charging service based on trigger type 140 | switch requestType { 141 | case smf_context.CHARGING_INIT: 142 | postChargingDataRequest := &ConvergedCharging.PostChargingDataRequest{ 143 | ChfConvergedChargingChargingDataRequest: req, 144 | } 145 | rspPost, localErr := client.DefaultApi.PostChargingData(ctx, postChargingDataRequest) 146 | 147 | switch err := localErr.(type) { 148 | case openapi.GenericOpenAPIError: 149 | switch errModel := err.Model().(type) { 150 | case ConvergedCharging.PostChargingDataError: 151 | return nil, &errModel.ProblemDetails, nil 152 | case error: 153 | return nil, openapi.ProblemDetailsSystemFailure(errModel.Error()), nil 154 | default: 155 | return nil, nil, openapi.ReportError("openapi error") 156 | } 157 | case error: 158 | return nil, openapi.ProblemDetailsSystemFailure(err.Error()), nil 159 | case nil: 160 | chargingDataRef := strings.Split(rspPost.Location, "/") 161 | smContext.ChargingDataRef = chargingDataRef[len(chargingDataRef)-1] 162 | return &rspPost.ChfConvergedChargingChargingDataResponse, nil, nil 163 | default: 164 | return nil, nil, openapi.ReportError("server no response") 165 | } 166 | case smf_context.CHARGING_UPDATE: 167 | updateChargingDataRequest := &ConvergedCharging.UpdateChargingDataRequest{ 168 | ChargingDataRef: &smContext.ChargingDataRef, 169 | ChfConvergedChargingChargingDataRequest: req, 170 | } 171 | rspUpdate, localErr := client.DefaultApi.UpdateChargingData(ctx, updateChargingDataRequest) 172 | 173 | switch err := localErr.(type) { 174 | case openapi.GenericOpenAPIError: 175 | switch errModel := err.Model().(type) { 176 | case ConvergedCharging.UpdateChargingDataError: 177 | return nil, &errModel.ProblemDetails, nil 178 | case error: 179 | return nil, openapi.ProblemDetailsSystemFailure(errModel.Error()), nil 180 | default: 181 | return nil, nil, openapi.ReportError("openapi error") 182 | } 183 | case error: 184 | return nil, openapi.ProblemDetailsSystemFailure(err.Error()), nil 185 | case nil: 186 | return &rspUpdate.ChfConvergedChargingChargingDataResponse, nil, nil 187 | default: 188 | return nil, nil, openapi.ReportError("server no response") 189 | } 190 | case smf_context.CHARGING_RELEASE: 191 | releaseChargingDataRequest := &ConvergedCharging.ReleaseChargingDataRequest{ 192 | ChargingDataRef: &smContext.ChargingDataRef, 193 | ChfConvergedChargingChargingDataRequest: req, 194 | } 195 | _, localErr := client.DefaultApi.ReleaseChargingData(ctx, releaseChargingDataRequest) 196 | 197 | switch err := localErr.(type) { 198 | case openapi.GenericOpenAPIError: 199 | switch errModel := err.Model().(type) { 200 | case ConvergedCharging.ReleaseChargingDataError: 201 | return nil, &errModel.ProblemDetails, nil 202 | case error: 203 | return nil, openapi.ProblemDetailsSystemFailure(errModel.Error()), nil 204 | default: 205 | return nil, nil, openapi.ReportError("openapi error") 206 | } 207 | case error: 208 | return nil, openapi.ProblemDetailsSystemFailure(err.Error()), nil 209 | case nil: 210 | return nil, nil, nil 211 | default: 212 | return nil, nil, openapi.ReportError("server no response") 213 | } 214 | default: 215 | return nil, nil, openapi.ReportError("invalid request type") 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /internal/sbi/consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "github.com/free5gc/openapi/amf/Communication" 5 | "github.com/free5gc/openapi/chf/ConvergedCharging" 6 | "github.com/free5gc/openapi/nrf/NFDiscovery" 7 | "github.com/free5gc/openapi/nrf/NFManagement" 8 | "github.com/free5gc/openapi/pcf/SMPolicyControl" 9 | "github.com/free5gc/openapi/smf/PDUSession" 10 | "github.com/free5gc/openapi/udm/SubscriberDataManagement" 11 | "github.com/free5gc/openapi/udm/UEContextManagement" 12 | "github.com/free5gc/smf/pkg/app" 13 | ) 14 | 15 | type Consumer struct { 16 | app.App 17 | 18 | // consumer services 19 | *nsmfService 20 | *namfService 21 | *nchfService 22 | *npcfService 23 | *nudmService 24 | *nnrfService 25 | } 26 | 27 | func NewConsumer(smf app.App) (*Consumer, error) { 28 | c := &Consumer{ 29 | App: smf, 30 | } 31 | 32 | c.nsmfService = &nsmfService{ 33 | consumer: c, 34 | PDUSessionClients: make(map[string]*PDUSession.APIClient), 35 | } 36 | 37 | c.namfService = &namfService{ 38 | consumer: c, 39 | CommunicationClients: make(map[string]*Communication.APIClient), 40 | } 41 | 42 | c.nchfService = &nchfService{ 43 | consumer: c, 44 | ConvergedChargingClients: make(map[string]*ConvergedCharging.APIClient), 45 | } 46 | 47 | c.nudmService = &nudmService{ 48 | consumer: c, 49 | SubscriberDataManagementClients: make(map[string]*SubscriberDataManagement.APIClient), 50 | UEContextManagementClients: make(map[string]*UEContextManagement.APIClient), 51 | } 52 | 53 | c.nnrfService = &nnrfService{ 54 | consumer: c, 55 | NFManagementClients: make(map[string]*NFManagement.APIClient), 56 | NFDiscoveryClients: make(map[string]*NFDiscovery.APIClient), 57 | } 58 | 59 | c.npcfService = &npcfService{ 60 | consumer: c, 61 | SMPolicyControlClients: make(map[string]*SMPolicyControl.APIClient), 62 | } 63 | 64 | return c, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/sbi/consumer/pcf_service_test.go: -------------------------------------------------------------------------------- 1 | package consumer_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "go.uber.org/mock/gomock" 9 | 10 | "github.com/free5gc/nas/nasType" 11 | "github.com/free5gc/openapi/models" 12 | smf_context "github.com/free5gc/smf/internal/context" 13 | "github.com/free5gc/smf/internal/sbi/consumer" 14 | "github.com/free5gc/smf/pkg/factory" 15 | "github.com/free5gc/smf/pkg/service" 16 | ) 17 | 18 | var testConfig = factory.Config{ 19 | Info: &factory.Info{ 20 | Version: "1.0.0", 21 | Description: "SMF procdeure test configuration", 22 | }, 23 | Configuration: &factory.Configuration{ 24 | Sbi: &factory.Sbi{ 25 | Scheme: "http", 26 | RegisterIPv4: "127.0.0.1", 27 | BindingIPv4: "127.0.0.1", 28 | Port: 8000, 29 | }, 30 | }, 31 | } 32 | 33 | func TestSendSMPolicyAssociationUpdateByUERequestModification(t *testing.T) { 34 | smf_context.InitSmfContext(&testConfig) 35 | 36 | testCases := []struct { 37 | name string 38 | smContext *smf_context.SMContext 39 | qosRules nasType.QoSRules 40 | qosFlowDescs nasType.QoSFlowDescs 41 | 42 | smPolicyDecision *models.SmPolicyDecision 43 | responseErr error 44 | }{ 45 | { 46 | name: "QoSRules is nil", 47 | smContext: smf_context.NewSMContext("imsi-208930000000001", 10), 48 | qosRules: nasType.QoSRules{}, 49 | qosFlowDescs: nasType.QoSFlowDescs{nasType.QoSFlowDesc{}}, 50 | smPolicyDecision: nil, 51 | responseErr: fmt.Errorf("QoS Rule not found"), 52 | }, 53 | { 54 | name: "QoSFlowDescs is nil", 55 | smContext: smf_context.NewSMContext("imsi-208930000000001", 10), 56 | qosRules: nasType.QoSRules{nasType.QoSRule{}}, 57 | qosFlowDescs: nasType.QoSFlowDescs{}, 58 | smPolicyDecision: nil, 59 | responseErr: fmt.Errorf("QoS Flow Description not found"), 60 | }, 61 | } 62 | 63 | mockSmf := service.NewMockSmfAppInterface(gomock.NewController(t)) 64 | consumer, errNewConsumer := consumer.NewConsumer(mockSmf) 65 | if errNewConsumer != nil { 66 | t.Fatalf("Failed to create consumer: %+v", errNewConsumer) 67 | } 68 | 69 | for _, tc := range testCases { 70 | t.Run(tc.name, func(t *testing.T) { 71 | smPolicyDecision, err := consumer.SendSMPolicyAssociationUpdateByUERequestModification( 72 | tc.smContext, tc.qosRules, tc.qosFlowDescs) 73 | 74 | require.Equal(t, tc.smPolicyDecision, smPolicyDecision) 75 | require.Equal(t, tc.responseErr.Error(), err.Error()) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/sbi/consumer/smf_service.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/free5gc/openapi" 8 | "github.com/free5gc/openapi/models" 9 | "github.com/free5gc/openapi/smf/PDUSession" 10 | "github.com/free5gc/smf/internal/logger" 11 | ) 12 | 13 | type nsmfService struct { 14 | consumer *Consumer 15 | 16 | PDUSessionMu sync.RWMutex 17 | 18 | PDUSessionClients map[string]*PDUSession.APIClient 19 | } 20 | 21 | func (s *nsmfService) getPDUSessionClient(uri string) *PDUSession.APIClient { 22 | if uri == "" { 23 | return nil 24 | } 25 | s.PDUSessionMu.RLock() 26 | client, ok := s.PDUSessionClients[uri] 27 | if ok { 28 | s.PDUSessionMu.RUnlock() 29 | return client 30 | } 31 | 32 | configuration := PDUSession.NewConfiguration() 33 | configuration.SetBasePath(uri) 34 | client = PDUSession.NewAPIClient(configuration) 35 | 36 | s.PDUSessionMu.RUnlock() 37 | s.PDUSessionMu.Lock() 38 | defer s.PDUSessionMu.Unlock() 39 | s.PDUSessionClients[uri] = client 40 | return client 41 | } 42 | 43 | func (s *nsmfService) SendSMContextStatusNotification(uri string) (*models.ProblemDetails, error) { 44 | if uri != "" { 45 | request := &PDUSession.PostSmContextsSmContextStatusNotificationPostRequest{ 46 | SmfPduSessionSmContextStatusNotification: &models.SmfPduSessionSmContextStatusNotification{ 47 | StatusInfo: &models.StatusInfo{ 48 | ResourceStatus: models.ResourceStatus_RELEASED, 49 | }, 50 | }, 51 | } 52 | 53 | client := s.getPDUSessionClient(uri) 54 | 55 | logger.CtxLog.Infoln("[SMF] Send SMContext Status Notification") 56 | _, localErr := client.SMContextsCollectionApi. 57 | PostSmContextsSmContextStatusNotificationPost(context.Background(), uri, request) 58 | 59 | switch err := localErr.(type) { 60 | case openapi.GenericOpenAPIError: 61 | switch errModel := err.Model().(type) { 62 | case PDUSession.PostSmContextsSmContextStatusNotificationPostError: 63 | return &errModel.ProblemDetails, nil 64 | case error: 65 | return openapi.ProblemDetailsSystemFailure(errModel.Error()), nil 66 | default: 67 | return nil, openapi.ReportError("openapi error") 68 | } 69 | case error: 70 | return openapi.ProblemDetailsSystemFailure(err.Error()), nil 71 | case nil: 72 | logger.PduSessLog.Tracef("Send SMContextStatus Notification Success") 73 | return nil, nil 74 | default: 75 | logger.PduSessLog.Warnf("Send SMContextStatus Notification Unknown Error: %+v", err) 76 | return nil, openapi.ReportError("server no response") 77 | } 78 | } 79 | return nil, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/sbi/processor/association.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/free5gc/nas/nasMessage" 9 | "github.com/free5gc/openapi/models" 10 | "github.com/free5gc/pfcp" 11 | "github.com/free5gc/pfcp/pfcpType" 12 | smf_context "github.com/free5gc/smf/internal/context" 13 | "github.com/free5gc/smf/internal/logger" 14 | "github.com/free5gc/smf/internal/pfcp/message" 15 | ) 16 | 17 | func (p *Processor) ToBeAssociatedWithUPF(smfPfcpContext context.Context, upf *smf_context.UPF) { 18 | var upfStr string 19 | if upf.NodeID.NodeIdType == pfcpType.NodeIdTypeFqdn { 20 | upfStr = fmt.Sprintf("[%s](%s)", upf.NodeID.FQDN, upf.NodeID.ResolveNodeIdToIp().String()) 21 | } else { 22 | upfStr = fmt.Sprintf("[%s]", upf.NodeID.ResolveNodeIdToIp().String()) 23 | } 24 | 25 | for { 26 | // check if SMF PFCP context (parent) was canceled 27 | // note: UPF AssociationContexts are children of smfPfcpContext 28 | select { 29 | case <-smfPfcpContext.Done(): 30 | logger.MainLog.Infoln("Canceled SMF PFCP context") 31 | return 32 | default: 33 | ensureSetupPfcpAssociation(smfPfcpContext, upf, upfStr) 34 | if smf_context.GetSelf().PfcpHeartbeatInterval == 0 { 35 | return 36 | } 37 | keepHeartbeatTo(upf, upfStr) 38 | // returns when UPF heartbeat loss is detected or association is canceled 39 | 40 | p.releaseAllResourcesOfUPF(upf, upfStr) 41 | } 42 | } 43 | } 44 | 45 | func (p *Processor) ReleaseAllResourcesOfUPF(upf *smf_context.UPF) { 46 | var upfStr string 47 | if upf.NodeID.NodeIdType == pfcpType.NodeIdTypeFqdn { 48 | upfStr = fmt.Sprintf("[%s](%s)", upf.NodeID.FQDN, upf.NodeID.ResolveNodeIdToIp().String()) 49 | } else { 50 | upfStr = fmt.Sprintf("[%s]", upf.NodeID.ResolveNodeIdToIp().String()) 51 | } 52 | p.releaseAllResourcesOfUPF(upf, upfStr) 53 | } 54 | 55 | func ensureSetupPfcpAssociation(parentContext context.Context, upf *smf_context.UPF, upfStr string) { 56 | alertTime := time.Now() 57 | alertInterval := smf_context.GetSelf().AssocFailAlertInterval 58 | retryInterval := smf_context.GetSelf().AssocFailRetryInterval 59 | for { 60 | err := setupPfcpAssociation(upf, upfStr) 61 | if err == nil { 62 | // success 63 | // assign UPF an AssociationContext, with SMF PFCP Context as parent 64 | upf.AssociationContext, upf.CancelAssociation = context.WithCancel(parentContext) 65 | return 66 | } 67 | logger.MainLog.Warnf("Failed to setup an association with UPF[%s], error:%+v", upfStr, err) 68 | now := time.Now() 69 | logger.MainLog.Debugf("now %+v, alertTime %+v", now, alertTime) 70 | if now.After(alertTime.Add(alertInterval)) { 71 | logger.MainLog.Errorf("ALERT for UPF[%s]", upfStr) 72 | alertTime = now 73 | } 74 | logger.MainLog.Debugf("Wait %+v until next retry attempt", retryInterval) 75 | timer := time.After(retryInterval) 76 | select { // no default case, either case needs to be true to continue 77 | case <-parentContext.Done(): 78 | logger.MainLog.Infoln("Canceled SMF PFCP context") 79 | return 80 | case <-timer: 81 | continue 82 | } 83 | } 84 | } 85 | 86 | func setupPfcpAssociation(upf *smf_context.UPF, upfStr string) error { 87 | logger.MainLog.Infof("Sending PFCP Association Request to UPF%s", upfStr) 88 | 89 | resMsg, err := message.SendPfcpAssociationSetupRequest(upf.NodeID) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | rsp := resMsg.PfcpMessage.Body.(pfcp.PFCPAssociationSetupResponse) 95 | 96 | if rsp.Cause == nil || rsp.Cause.CauseValue != pfcpType.CauseRequestAccepted { 97 | return fmt.Errorf("received PFCP Association Setup Not Accepted Response from UPF%s", upfStr) 98 | } 99 | 100 | nodeID := rsp.NodeID 101 | if nodeID == nil { 102 | return fmt.Errorf("pfcp association needs NodeID") 103 | } 104 | 105 | logger.MainLog.Infof("Received PFCP Association Setup Accepted Response from UPF%s", upfStr) 106 | logger.MainLog.Infof("UPF(%s) setup association", upf.NodeID.ResolveNodeIdToIp().String()) 107 | 108 | return nil 109 | } 110 | 111 | func keepHeartbeatTo(upf *smf_context.UPF, upfStr string) { 112 | for { 113 | err := doPfcpHeartbeat(upf, upfStr) 114 | if err != nil { 115 | logger.MainLog.Errorf("PFCP Heartbeat error: %v", err) 116 | return 117 | } 118 | 119 | timer := time.After(smf_context.GetSelf().PfcpHeartbeatInterval) 120 | select { 121 | case <-upf.AssociationContext.Done(): 122 | logger.MainLog.Infof("Canceled association to UPF[%s]", upfStr) 123 | return 124 | case <-timer: 125 | continue 126 | } 127 | } 128 | } 129 | 130 | func doPfcpHeartbeat(upf *smf_context.UPF, upfStr string) error { 131 | if err := upf.IsAssociated(); err != nil { 132 | return fmt.Errorf("Cancel heartbeat: %+v", err) 133 | } 134 | 135 | logger.MainLog.Debugf("Sending PFCP Heartbeat Request to UPF%s", upfStr) 136 | 137 | resMsg, err := message.SendPfcpHeartbeatRequest(upf) 138 | if err != nil { 139 | upf.CancelAssociation() 140 | upf.RecoveryTimeStamp = time.Time{} 141 | return fmt.Errorf("SendPfcpHeartbeatRequest error: %w", err) 142 | } 143 | 144 | rsp := resMsg.PfcpMessage.Body.(pfcp.HeartbeatResponse) 145 | if rsp.RecoveryTimeStamp == nil { 146 | logger.MainLog.Warnf("Received PFCP Heartbeat Response without timestamp from UPF%s", upfStr) 147 | return nil 148 | } 149 | 150 | logger.MainLog.Debugf("Received PFCP Heartbeat Response from UPF%s", upfStr) 151 | if upf.RecoveryTimeStamp.IsZero() { 152 | // first receive 153 | upf.RecoveryTimeStamp = rsp.RecoveryTimeStamp.RecoveryTimeStamp 154 | } else if upf.RecoveryTimeStamp.Before(rsp.RecoveryTimeStamp.RecoveryTimeStamp) { 155 | // received a newer recovery timestamp 156 | upf.CancelAssociation() 157 | upf.RecoveryTimeStamp = time.Time{} 158 | return fmt.Errorf("received PFCP Heartbeat Response RecoveryTimeStamp has been updated") 159 | } 160 | return nil 161 | } 162 | 163 | func (p *Processor) releaseAllResourcesOfUPF(upf *smf_context.UPF, upfStr string) { 164 | logger.MainLog.Infof("Release all resources of UPF %s", upfStr) 165 | 166 | upf.ProcEachSMContext(func(smContext *smf_context.SMContext) { 167 | smContext.SMLock.Lock() 168 | defer smContext.SMLock.Unlock() 169 | switch smContext.State() { 170 | case smf_context.Active, smf_context.ModificationPending, smf_context.PFCPModification: 171 | needToSendNotify, removeContext := p.requestAMFToReleasePDUResources(smContext) 172 | if needToSendNotify { 173 | p.SendReleaseNotification(smContext) 174 | } 175 | if removeContext { 176 | // Notification has already been sent, if it is needed 177 | p.RemoveSMContextFromAllNF(smContext, false) 178 | } 179 | } 180 | }) 181 | } 182 | 183 | func (p *Processor) requestAMFToReleasePDUResources( 184 | smContext *smf_context.SMContext, 185 | ) (sendNotify bool, releaseContext bool) { 186 | n1n2Request := models.N1N2MessageTransferRequest{} 187 | // TS 23.502 4.3.4.2 3b. Send Namf_Communication_N1N2MessageTransfer Request, SMF->AMF 188 | n1n2Request.JsonData = &models.N1N2MessageTransferReqData{ 189 | PduSessionId: smContext.PDUSessionID, 190 | SkipInd: true, 191 | } 192 | cause := nasMessage.Cause5GSMNetworkFailure 193 | if buf, err := smf_context.BuildGSMPDUSessionReleaseCommand(smContext, cause, false); err != nil { 194 | logger.MainLog.Errorf("Build GSM PDUSessionReleaseCommand failed: %+v", err) 195 | } else { 196 | n1n2Request.BinaryDataN1Message = buf 197 | n1n2Request.JsonData.N1MessageContainer = &models.N1MessageContainer{ 198 | N1MessageClass: "SM", 199 | N1MessageContent: &models.RefToBinaryData{ContentId: "GSM_NAS"}, 200 | } 201 | } 202 | if smContext.UpCnxState != models.UpCnxState_DEACTIVATED { 203 | if buf, err := smf_context.BuildPDUSessionResourceReleaseCommandTransfer(smContext); err != nil { 204 | logger.MainLog.Errorf("Build PDUSessionResourceReleaseCommandTransfer failed: %+v", err) 205 | } else { 206 | n1n2Request.BinaryDataN2Information = buf 207 | n1n2Request.JsonData.N2InfoContainer = &models.N2InfoContainer{ 208 | N2InformationClass: models.N2InformationClass_SM, 209 | SmInfo: &models.N2SmInformation{ 210 | PduSessionId: smContext.PDUSessionID, 211 | N2InfoContent: &models.N2InfoContent{ 212 | NgapIeType: models.AmfCommunicationNgapIeType_PDU_RES_REL_CMD, 213 | NgapData: &models.RefToBinaryData{ 214 | ContentId: "N2SmInformation", 215 | }, 216 | }, 217 | SNssai: smContext.SNssai, 218 | }, 219 | } 220 | } 221 | } 222 | 223 | ctx, _, errToken := smf_context.GetSelf().GetTokenCtx(models.ServiceName_NAMF_COMM, models.NrfNfManagementNfType_AMF) 224 | if errToken != nil { 225 | return false, false 226 | } 227 | 228 | rspData, err := p.Consumer(). 229 | N1N2MessageTransfer(ctx, smContext.Supi, n1n2Request, smContext.CommunicationClientApiPrefix) 230 | 231 | if err != nil || rspData == nil { 232 | logger.ConsumerLog.Warnf("N1N2MessageTransfer for RequestAMFToReleasePDUResources failed: %+v", err) 233 | // keep SM Context to avoid inconsistency with AMF 234 | smContext.SetState(smf_context.InActive) 235 | } else { 236 | if rspData.Cause == models.N1N2MessageTransferCause_N1_MSG_NOT_TRANSFERRED { 237 | // the PDU Session Release Command was not transferred to the UE since it is in CM-IDLE state. 238 | // ref. step3b of "4.3.4.2 UE or network requested PDU Session Release for Non-Roaming and 239 | // Roaming with Local Breakout" in TS23.502 240 | // it is needed to remove both AMF's and SMF's SM Contexts immediately 241 | smContext.SetState(smf_context.InActive) 242 | return true, true 243 | } else if rspData.Cause == models.N1N2MessageTransferCause_N1_N2_TRANSFER_INITIATED { 244 | // wait for N2 PDU Session Release Response 245 | smContext.SetState(smf_context.InActivePending) 246 | } else { 247 | // other causes are unexpected. 248 | // keep SM Context to avoid inconsistency with AMF 249 | smContext.SetState(smf_context.InActive) 250 | } 251 | } 252 | return false, false 253 | } 254 | -------------------------------------------------------------------------------- /internal/sbi/processor/gsm_handler_test.go: -------------------------------------------------------------------------------- 1 | package processor_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/free5gc/nas/nasType" 10 | "github.com/free5gc/openapi/models" 11 | "github.com/free5gc/smf/internal/context" 12 | "github.com/free5gc/util/idgenerator" 13 | ) 14 | 15 | func TestBuildNASPacketFilterFromPacketFilterInfo(t *testing.T) { 16 | testCases := []struct { 17 | name string 18 | packetFilter []nasType.PacketFilter 19 | flowInfo models.FlowInformation 20 | }{ 21 | { 22 | name: "MatchAll", 23 | packetFilter: []nasType.PacketFilter{ 24 | { 25 | Direction: nasType.PacketFilterDirectionBidirectional, 26 | Components: nasType.PacketFilterComponentList{ 27 | &nasType.PacketFilterMatchAll{}, 28 | }, 29 | }, 30 | }, 31 | flowInfo: models.FlowInformation{ 32 | FlowDirection: models.FlowDirection_BIDIRECTIONAL, 33 | FlowDescription: "permit out ip from any to assigned", 34 | }, 35 | }, 36 | { 37 | name: "MatchIPNet1", 38 | packetFilter: []nasType.PacketFilter{ 39 | { 40 | Direction: nasType.PacketFilterDirectionUplink, 41 | Components: nasType.PacketFilterComponentList{ 42 | &nasType.PacketFilterIPv4LocalAddress{ 43 | Address: net.ParseIP("192.168.0.0").To4(), 44 | Mask: net.IPv4Mask(255, 255, 0, 0), 45 | }, 46 | }, 47 | }, 48 | }, 49 | flowInfo: models.FlowInformation{ 50 | FlowDirection: models.FlowDirection_UPLINK, 51 | FlowDescription: "permit out ip from any to 192.168.0.0/16", 52 | }, 53 | }, 54 | { 55 | name: "MatchIPNet2", 56 | packetFilter: []nasType.PacketFilter{ 57 | { 58 | Direction: nasType.PacketFilterDirectionBidirectional, 59 | Components: nasType.PacketFilterComponentList{ 60 | &nasType.PacketFilterIPv4LocalAddress{ 61 | Address: net.ParseIP("192.168.0.0").To4(), 62 | Mask: net.IPv4Mask(255, 255, 0, 0), 63 | }, 64 | &nasType.PacketFilterIPv4RemoteAddress{ 65 | Address: net.ParseIP("10.160.20.0").To4(), 66 | Mask: net.IPv4Mask(255, 255, 255, 0), 67 | }, 68 | }, 69 | }, 70 | }, 71 | flowInfo: models.FlowInformation{ 72 | FlowDirection: models.FlowDirection_BIDIRECTIONAL, 73 | FlowDescription: "permit out ip from 10.160.20.0/24 to 192.168.0.0/16", 74 | }, 75 | }, 76 | { 77 | name: "MatchIPNetPort", 78 | packetFilter: []nasType.PacketFilter{ 79 | { 80 | Direction: nasType.PacketFilterDirectionBidirectional, 81 | Components: nasType.PacketFilterComponentList{ 82 | &nasType.PacketFilterIPv4LocalAddress{ 83 | Address: net.ParseIP("192.168.0.0").To4(), 84 | Mask: net.IPv4Mask(255, 255, 0, 0), 85 | }, 86 | &nasType.PacketFilterSingleLocalPort{ 87 | Value: 8000, 88 | }, 89 | &nasType.PacketFilterIPv4RemoteAddress{ 90 | Address: net.ParseIP("10.160.20.0").To4(), 91 | Mask: net.IPv4Mask(255, 255, 255, 0), 92 | }, 93 | }, 94 | }, 95 | }, 96 | flowInfo: models.FlowInformation{ 97 | FlowDirection: models.FlowDirection_BIDIRECTIONAL, 98 | FlowDescription: "permit out ip from 10.160.20.0/24 to 192.168.0.0/16 8000", 99 | }, 100 | }, 101 | { 102 | name: "MatchIPNetPortRanges", 103 | packetFilter: []nasType.PacketFilter{ 104 | { 105 | Direction: nasType.PacketFilterDirectionDownlink, 106 | Components: nasType.PacketFilterComponentList{ 107 | &nasType.PacketFilterIPv4LocalAddress{ 108 | Address: net.ParseIP("192.168.0.0").To4(), 109 | Mask: net.IPv4Mask(255, 255, 0, 0), 110 | }, 111 | &nasType.PacketFilterLocalPortRange{ 112 | LowLimit: 3000, 113 | HighLimit: 8000, 114 | }, 115 | &nasType.PacketFilterIPv4RemoteAddress{ 116 | Address: net.ParseIP("10.160.20.0").To4(), 117 | Mask: net.IPv4Mask(255, 255, 255, 0), 118 | }, 119 | }, 120 | }, 121 | }, 122 | flowInfo: models.FlowInformation{ 123 | FlowDirection: models.FlowDirection_DOWNLINK, 124 | FlowDescription: "permit out ip from 10.160.20.0/24 to 192.168.0.0/16 3000-8000", 125 | }, 126 | }, 127 | { 128 | name: "MatchIPNetPortRanges2", 129 | packetFilter: []nasType.PacketFilter{ 130 | { 131 | Direction: nasType.PacketFilterDirectionDownlink, 132 | Components: nasType.PacketFilterComponentList{ 133 | &nasType.PacketFilterIPv4LocalAddress{ 134 | Address: net.ParseIP("192.168.0.0").To4(), 135 | Mask: net.IPv4Mask(255, 255, 0, 0), 136 | }, 137 | &nasType.PacketFilterLocalPortRange{ 138 | LowLimit: 6000, 139 | HighLimit: 8000, 140 | }, 141 | &nasType.PacketFilterIPv4RemoteAddress{ 142 | Address: net.ParseIP("10.160.20.0").To4(), 143 | Mask: net.IPv4Mask(255, 255, 255, 0), 144 | }, 145 | &nasType.PacketFilterRemotePortRange{ 146 | LowLimit: 3000, 147 | HighLimit: 4000, 148 | }, 149 | }, 150 | }, 151 | }, 152 | flowInfo: models.FlowInformation{ 153 | FlowDirection: models.FlowDirection_DOWNLINK, 154 | FlowDescription: "permit out ip from 10.160.20.0/24 3000-4000 to 192.168.0.0/16 6000-8000", 155 | }, 156 | }, 157 | { 158 | name: "MatchIPNetPortRanges3", 159 | packetFilter: []nasType.PacketFilter{ 160 | { 161 | Direction: nasType.PacketFilterDirectionDownlink, 162 | Components: nasType.PacketFilterComponentList{ 163 | &nasType.PacketFilterIPv4LocalAddress{ 164 | Address: net.ParseIP("192.168.0.0").To4(), 165 | Mask: net.IPv4Mask(255, 255, 0, 0), 166 | }, 167 | &nasType.PacketFilterLocalPortRange{ 168 | LowLimit: 6000, 169 | HighLimit: 7000, 170 | }, 171 | &nasType.PacketFilterIPv4RemoteAddress{ 172 | Address: net.ParseIP("10.160.20.0").To4(), 173 | Mask: net.IPv4Mask(255, 255, 255, 0), 174 | }, 175 | &nasType.PacketFilterRemotePortRange{ 176 | LowLimit: 3000, 177 | HighLimit: 4000, 178 | }, 179 | }, 180 | }, 181 | { 182 | Direction: nasType.PacketFilterDirectionDownlink, 183 | Components: nasType.PacketFilterComponentList{ 184 | &nasType.PacketFilterIPv4LocalAddress{ 185 | Address: net.ParseIP("192.168.0.0").To4(), 186 | Mask: net.IPv4Mask(255, 255, 0, 0), 187 | }, 188 | &nasType.PacketFilterSingleLocalPort{ 189 | Value: 8000, 190 | }, 191 | &nasType.PacketFilterIPv4RemoteAddress{ 192 | Address: net.ParseIP("10.160.20.0").To4(), 193 | Mask: net.IPv4Mask(255, 255, 255, 0), 194 | }, 195 | &nasType.PacketFilterRemotePortRange{ 196 | LowLimit: 3000, 197 | HighLimit: 4000, 198 | }, 199 | }, 200 | }, 201 | }, 202 | flowInfo: models.FlowInformation{ 203 | FlowDirection: models.FlowDirection_DOWNLINK, 204 | FlowDescription: "permit out ip from 10.160.20.0/24 3000-4000 to 192.168.0.0/16 6000-7000,8000", 205 | }, 206 | }, 207 | { 208 | name: "MatchIPNetPortRanges4", 209 | packetFilter: []nasType.PacketFilter{ 210 | { 211 | Direction: nasType.PacketFilterDirectionDownlink, 212 | Components: nasType.PacketFilterComponentList{ 213 | &nasType.PacketFilterIPv4LocalAddress{ 214 | Address: net.ParseIP("192.168.0.0").To4(), 215 | Mask: net.IPv4Mask(255, 255, 0, 0), 216 | }, 217 | &nasType.PacketFilterLocalPortRange{ 218 | LowLimit: 6000, 219 | HighLimit: 7000, 220 | }, 221 | &nasType.PacketFilterIPv4RemoteAddress{ 222 | Address: net.ParseIP("10.160.20.0").To4(), 223 | Mask: net.IPv4Mask(255, 255, 255, 0), 224 | }, 225 | &nasType.PacketFilterRemotePortRange{ 226 | LowLimit: 3000, 227 | HighLimit: 4000, 228 | }, 229 | }, 230 | }, 231 | { 232 | Direction: nasType.PacketFilterDirectionDownlink, 233 | Components: nasType.PacketFilterComponentList{ 234 | &nasType.PacketFilterIPv4LocalAddress{ 235 | Address: net.ParseIP("192.168.0.0").To4(), 236 | Mask: net.IPv4Mask(255, 255, 0, 0), 237 | }, 238 | &nasType.PacketFilterLocalPortRange{ 239 | LowLimit: 6000, 240 | HighLimit: 7000, 241 | }, 242 | &nasType.PacketFilterIPv4RemoteAddress{ 243 | Address: net.ParseIP("10.160.20.0").To4(), 244 | Mask: net.IPv4Mask(255, 255, 255, 0), 245 | }, 246 | &nasType.PacketFilterSingleRemotePort{ 247 | Value: 5000, 248 | }, 249 | }, 250 | }, 251 | }, 252 | flowInfo: models.FlowInformation{ 253 | FlowDirection: models.FlowDirection_DOWNLINK, 254 | FlowDescription: "permit out ip from 10.160.20.0/24 3000-4000,5000 to 192.168.0.0/16 6000-7000", 255 | }, 256 | }, 257 | } 258 | 259 | for _, tc := range testCases { 260 | t.Run(tc.name, func(t *testing.T) { 261 | smCtx := &context.SMContext{ 262 | PacketFilterIDGenerator: idgenerator.NewGenerator(1, 255), 263 | PacketFilterIDToNASPFID: make(map[string]uint8), 264 | } 265 | packetFilters, err := context.BuildNASPacketFiltersFromFlowInformation(&tc.flowInfo, smCtx) 266 | require.NoError(t, err) 267 | 268 | for i, pf := range packetFilters { 269 | require.Equal(t, tc.packetFilter[i].Direction, pf.Direction) 270 | require.Equal(t, tc.packetFilter[i].Components, pf.Components) 271 | } 272 | }) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /internal/sbi/processor/notifier.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/free5gc/openapi" 11 | "github.com/free5gc/openapi/models" 12 | "github.com/free5gc/openapi/smf/EventExposure" 13 | smf_context "github.com/free5gc/smf/internal/context" 14 | "github.com/free5gc/smf/internal/logger" 15 | ) 16 | 17 | func (p *Processor) HandleChargingNotification( 18 | c *gin.Context, 19 | chargingNotifyRequest models.ChargingNotifyRequest, 20 | smContextRef string, 21 | ) { 22 | logger.ChargingLog.Info("Handle Charging Notification") 23 | 24 | problemDetails := p.chargingNotificationProcedure(chargingNotifyRequest, smContextRef) 25 | if problemDetails == nil { 26 | c.Status(http.StatusNoContent) 27 | return 28 | } 29 | c.JSON(int(problemDetails.Status), problemDetails) 30 | } 31 | 32 | // While receive Charging Notification from CHF, SMF will send Charging Information to CHF and update UPF 33 | // The Charging Notification will be sent when CHF found the changes of the quota file. 34 | func (p *Processor) chargingNotificationProcedure( 35 | req models.ChargingNotifyRequest, smContextRef string, 36 | ) *models.ProblemDetails { 37 | if smContext := smf_context.GetSMContextByRef(smContextRef); smContext != nil { 38 | smContext.SMLock.Lock() 39 | defer smContext.SMLock.Unlock() 40 | upfUrrMap := make(map[string][]*smf_context.URR) 41 | for _, reauthorizeDetail := range req.ReauthorizationDetails { 42 | rg := reauthorizeDetail.RatingGroup 43 | logger.ChargingLog.Infof("Force update charging information for rating group %d", rg) 44 | for _, urr := range smContext.UrrUpfMap { 45 | chgInfo := smContext.ChargingInfo[urr.URRID] 46 | if chgInfo.RatingGroup == rg || 47 | chgInfo.ChargingLevel == smf_context.PduSessionCharging { 48 | logger.ChargingLog.Tracef("Query URR (%d) for Rating Group (%d)", urr.URRID, rg) 49 | upfId := smContext.ChargingInfo[urr.URRID].UpfId 50 | upfUrrMap[upfId] = append(upfUrrMap[upfId], urr) 51 | } 52 | } 53 | } 54 | for upfId, urrList := range upfUrrMap { 55 | upf := smf_context.GetUpfById(upfId) 56 | if upf == nil { 57 | logger.ChargingLog.Warnf("Cound not find upf %s", upfId) 58 | continue 59 | } 60 | QueryReport(smContext, upf, urrList, models.ChfConvergedChargingTriggerType_FORCED_REAUTHORISATION) 61 | } 62 | p.ReportUsageAndUpdateQuota(smContext) 63 | } else { 64 | detail := fmt.Sprintf("SM Context [%s] Not Found ", smContextRef) 65 | return openapi.ProblemDetailsDataNotFound(detail) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (p *Processor) HandleSMPolicyUpdateNotify( 72 | c *gin.Context, 73 | request models.SmPolicyNotification, 74 | smContextRef string, 75 | ) { 76 | logger.PduSessLog.Infoln("In HandleSMPolicyUpdateNotify") 77 | decision := request.SmPolicyDecision 78 | smContext := smf_context.GetSMContextByRef(smContextRef) 79 | 80 | if smContext == nil { 81 | logger.PduSessLog.Errorf("SMContext[%s] not found", smContextRef) 82 | c.Status(http.StatusBadRequest) 83 | return 84 | } 85 | 86 | smContext.SMLock.Lock() 87 | defer smContext.SMLock.Unlock() 88 | 89 | smContext.CheckState(smf_context.Active) 90 | // Wait till the state becomes Active again 91 | // TODO: implement waiting in concurrent architecture 92 | 93 | smContext.SetState(smf_context.ModificationPending) 94 | 95 | // Update SessionRule from decision 96 | if err := smContext.ApplySessionRules(decision); err != nil { 97 | // TODO: Fill the error body 98 | smContext.Log.Errorf("SMPolicyUpdateNotify err: %v", err) 99 | c.Status(http.StatusBadRequest) 100 | return 101 | } 102 | 103 | // TODO: Response data type - 104 | // [200 OK] UeCampingRep 105 | // [200 OK] array(PartialSuccessReport) 106 | // [400 Bad Request] ErrorReport 107 | if err := smContext.ApplyPccRules(decision); err != nil { 108 | smContext.Log.Errorf("apply sm policy decision error: %+v", err) 109 | // TODO: Fill the error body 110 | c.Status(http.StatusBadRequest) 111 | return 112 | } 113 | 114 | smContext.SendUpPathChgNotification("EARLY", SendUpPathChgEventExposureNotification) 115 | 116 | ActivateUPFSession(smContext, nil) 117 | 118 | smContext.SendUpPathChgNotification("LATE", SendUpPathChgEventExposureNotification) 119 | 120 | smContext.PostRemoveDataPath() 121 | 122 | c.Status(http.StatusNoContent) 123 | } 124 | 125 | func SendUpPathChgEventExposureNotification( 126 | uri string, notification *models.NsmfEventExposureNotification, 127 | ) { 128 | configuration := EventExposure.NewConfiguration() 129 | client := EventExposure.NewAPIClient(configuration) 130 | request := &EventExposure.CreateIndividualSubcriptionMyNotificationPostRequest{ 131 | NsmfEventExposureNotification: notification, 132 | } 133 | _, err := client. 134 | SubscriptionsCollectionApi. 135 | CreateIndividualSubcriptionMyNotificationPost(context.Background(), uri, request) 136 | 137 | switch err := err.(type) { 138 | case openapi.GenericOpenAPIError: 139 | logger.PduSessLog.Warnf("SMF Event Exposure Notification Error[%s]", err.Error()) 140 | case error: 141 | logger.PduSessLog.Warnf("SMF Event Exposure Notification Failed[%s]", err.Error()) 142 | case nil: 143 | logger.PduSessLog.Tracef("SMF Event Exposure Notification Success") 144 | default: 145 | logger.PduSessLog.Warnf("SMF Event Exposure Notification Unknown Error: %+v", err) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/sbi/processor/oam.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/free5gc/openapi/models" 10 | "github.com/free5gc/smf/internal/context" 11 | "github.com/free5gc/smf/pkg/factory" 12 | ) 13 | 14 | type PDUSessionInfo struct { 15 | Supi string 16 | PDUSessionID string 17 | Dnn string 18 | Sst string 19 | Sd string 20 | AnType models.AccessType 21 | PDUAddress string 22 | SessionRule models.SessionRule 23 | UpCnxState models.UpCnxState 24 | Tunnel context.UPTunnel 25 | } 26 | 27 | func (p *Processor) HandleOAMGetUEPDUSessionInfo(c *gin.Context, smContextRef string) { 28 | smContext := context.GetSMContextByRef(smContextRef) 29 | if smContext == nil { 30 | c.JSON(http.StatusNotFound, nil) 31 | return 32 | } 33 | 34 | pduSessionInfo := &PDUSessionInfo{ 35 | Supi: smContext.Supi, 36 | PDUSessionID: strconv.Itoa(int(smContext.PDUSessionID)), 37 | Dnn: smContext.Dnn, 38 | Sst: strconv.Itoa(int(smContext.SNssai.Sst)), 39 | Sd: smContext.SNssai.Sd, 40 | AnType: smContext.AnType, 41 | PDUAddress: smContext.PDUAddress.String(), 42 | UpCnxState: smContext.UpCnxState, 43 | // Tunnel: context.UPTunnel{ 44 | // //UpfRoot: smContext.Tunnel.UpfRoot, 45 | // ULCLRoot: smContext.Tunnel.UpfRoot, 46 | // }, 47 | } 48 | c.JSON(http.StatusOK, pduSessionInfo) 49 | } 50 | 51 | func (p *Processor) HandleGetSMFUserPlaneInfo(c *gin.Context) { 52 | c.JSON(http.StatusOK, factory.SmfConfig.Configuration.UserPlaneInformation) 53 | } 54 | -------------------------------------------------------------------------------- /internal/sbi/processor/processor.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "github.com/free5gc/smf/internal/sbi/consumer" 5 | "github.com/free5gc/smf/pkg/app" 6 | ) 7 | 8 | const ( 9 | CONTEXT_NOT_FOUND = "CONTEXT_NOT_FOUND" 10 | ) 11 | 12 | type ProcessorSmf interface { 13 | app.App 14 | 15 | Consumer() *consumer.Consumer 16 | } 17 | 18 | type Processor struct { 19 | ProcessorSmf 20 | } 21 | 22 | func NewProcessor(smf ProcessorSmf) (*Processor, error) { 23 | p := &Processor{ 24 | ProcessorSmf: smf, 25 | } 26 | return p, nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/sbi/processor/sm_common.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | smf_context "github.com/free5gc/smf/internal/context" 5 | ) 6 | 7 | func (p *Processor) RemoveSMContextFromAllNF(smContext *smf_context.SMContext, sendNotification bool) { 8 | smContext.SetState(smf_context.InActive) 9 | // remove SM Policy Association 10 | if smContext.SMPolicyID != "" { 11 | if err := p.Consumer().SendSMPolicyAssociationTermination(smContext); err != nil { 12 | smContext.Log.Errorf("SM Policy Termination failed: %s", err) 13 | } else { 14 | smContext.SMPolicyID = "" 15 | } 16 | } 17 | 18 | if smf_context.GetSelf().Ues.UeExists(smContext.Supi) { 19 | problemDetails, err := p.Consumer().UnSubscribe(smContext) 20 | if problemDetails != nil { 21 | smContext.Log.Errorf("SDM UnSubscription Failed Problem[%+v]", problemDetails) 22 | } else if err != nil { 23 | smContext.Log.Errorf("SDM UnSubscription Error[%+v]", err) 24 | } 25 | } 26 | 27 | // Because the amfUE who called this SMF API is being locked until the API Handler returns, 28 | // sending SMContext Status Notification should run asynchronously 29 | // so that this function returns immediately. 30 | go p.sendSMContextStatusNotificationAndRemoveSMContext(smContext, sendNotification) 31 | } 32 | 33 | func (p *Processor) sendSMContextStatusNotificationAndRemoveSMContext( 34 | smContext *smf_context.SMContext, sendNotification bool, 35 | ) { 36 | smContext.SMLock.Lock() 37 | defer smContext.SMLock.Unlock() 38 | 39 | if sendNotification && len(smContext.SmStatusNotifyUri) != 0 { 40 | p.SendReleaseNotification(smContext) 41 | } 42 | 43 | smf_context.RemoveSMContext(smContext.Ref) 44 | } 45 | 46 | func (p *Processor) SendReleaseNotification(smContext *smf_context.SMContext) { 47 | // Use go routine to send Notification to prevent blocking the handling process 48 | problemDetails, err := p.Consumer().SendSMContextStatusNotification(smContext.SmStatusNotifyUri) 49 | if problemDetails != nil || err != nil { 50 | if problemDetails != nil { 51 | smContext.Log.Warnf("Send SMContext Status Notification Problem[%+v]", problemDetails) 52 | } 53 | 54 | if err != nil { 55 | smContext.Log.Warnf("Send SMContext Status Notification Error[%v]", err) 56 | } 57 | } else { 58 | smContext.Log.Traceln("Send SMContext Status Notification successfully") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/sbi/routes.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type Route struct { 6 | Name string 7 | Method string 8 | Pattern string 9 | APIFunc gin.HandlerFunc 10 | } 11 | 12 | func applyRoutes(group *gin.RouterGroup, routes []Route) { 13 | for _, route := range routes { 14 | switch route.Method { 15 | case "GET": 16 | group.GET(route.Pattern, route.APIFunc) 17 | case "POST": 18 | group.POST(route.Pattern, route.APIFunc) 19 | case "PUT": 20 | group.PUT(route.Pattern, route.APIFunc) 21 | case "PATCH": 22 | group.PATCH(route.Pattern, route.APIFunc) 23 | case "DELETE": 24 | group.DELETE(route.Pattern, route.APIFunc) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/sbi/server.go: -------------------------------------------------------------------------------- 1 | package sbi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "runtime/debug" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | "github.com/free5gc/openapi/models" 14 | smf_context "github.com/free5gc/smf/internal/context" 15 | "github.com/free5gc/smf/internal/logger" 16 | "github.com/free5gc/smf/internal/sbi/consumer" 17 | "github.com/free5gc/smf/internal/sbi/processor" 18 | util_oauth "github.com/free5gc/smf/internal/util/oauth" 19 | "github.com/free5gc/smf/pkg/app" 20 | "github.com/free5gc/smf/pkg/factory" 21 | "github.com/free5gc/util/httpwrapper" 22 | logger_util "github.com/free5gc/util/logger" 23 | ) 24 | 25 | const ( 26 | APPLICATION_JSON = "application/json" 27 | MULTIPART_RELATED = "multipart/related" 28 | ) 29 | 30 | type ServerSmf interface { 31 | app.App 32 | 33 | Consumer() *consumer.Consumer 34 | Processor() *processor.Processor 35 | CancelContext() context.Context 36 | } 37 | 38 | type Server struct { 39 | ServerSmf 40 | 41 | httpServer *http.Server 42 | router *gin.Engine 43 | } 44 | 45 | func NewServer(smf ServerSmf, tlsKeyLogPath string) (*Server, error) { 46 | s := &Server{ 47 | ServerSmf: smf, 48 | } 49 | 50 | smf_context.InitSmfContext(factory.SmfConfig) 51 | // allocate id for each upf 52 | smf_context.AllocateUPFID() 53 | smf_context.InitSMFUERouting(factory.UERoutingConfig) 54 | 55 | s.router = newRouter(s) 56 | 57 | bindAddr := fmt.Sprintf("%s:%d", s.Context().BindingIPv4, s.Context().SBIPort) 58 | var err error 59 | if s.httpServer, err = httpwrapper.NewHttp2Server(bindAddr, tlsKeyLogPath, s.router); err != nil { 60 | logger.InitLog.Errorf("Initialize HTTP server failed: %v", err) 61 | return nil, err 62 | } 63 | 64 | return s, nil 65 | } 66 | 67 | func newRouter(s *Server) *gin.Engine { 68 | router := logger_util.NewGinWithLogrus(logger.GinLog) 69 | 70 | smfCallbackGroup := router.Group(factory.SmfCallbackUriPrefix) 71 | smfCallbackRoutes := s.getCallbackRoutes() 72 | applyRoutes(smfCallbackGroup, smfCallbackRoutes) 73 | 74 | upiGroup := router.Group(factory.UpiUriPrefix) 75 | upiRoutes := s.getUPIRoutes() 76 | applyRoutes(upiGroup, upiRoutes) 77 | 78 | for _, serviceName := range factory.SmfConfig.Configuration.ServiceNameList { 79 | switch models.ServiceName(serviceName) { 80 | case models.ServiceName_NSMF_PDUSESSION: 81 | smfPDUSessionGroup := router.Group(factory.SmfPdusessionResUriPrefix) 82 | smfPDUSessionRoutes := s.getPDUSessionRoutes() 83 | routerAuthorizationCheck := util_oauth.NewRouterAuthorizationCheck(models.ServiceName_NSMF_PDUSESSION) 84 | smfPDUSessionGroup.Use(func(c *gin.Context) { 85 | routerAuthorizationCheck.Check(c, smf_context.GetSelf()) 86 | }) 87 | applyRoutes(smfPDUSessionGroup, smfPDUSessionRoutes) 88 | case models.ServiceName_NSMF_EVENT_EXPOSURE: 89 | smfEventExposureGroup := router.Group(factory.SmfEventExposureResUriPrefix) 90 | smfEventExposureRoutes := s.getEventExposureRoutes() 91 | routerAuthorizationCheck := util_oauth.NewRouterAuthorizationCheck(models.ServiceName_NSMF_EVENT_EXPOSURE) 92 | smfEventExposureGroup.Use(func(c *gin.Context) { 93 | routerAuthorizationCheck.Check(c, smf_context.GetSelf()) 94 | }) 95 | applyRoutes(smfEventExposureGroup, smfEventExposureRoutes) 96 | case models.ServiceName_NSMF_OAM: 97 | smfOAMGroup := router.Group(factory.SmfOamUriPrefix) 98 | smfOAMRoutes := s.getOAMRoutes() 99 | routerAuthorizationCheck := util_oauth.NewRouterAuthorizationCheck(models.ServiceName_NSMF_OAM) 100 | smfOAMGroup.Use(func(c *gin.Context) { 101 | routerAuthorizationCheck.Check(c, smf_context.GetSelf()) 102 | }) 103 | applyRoutes(smfOAMGroup, smfOAMRoutes) 104 | } 105 | } 106 | 107 | return router 108 | } 109 | 110 | func (s *Server) Run(traceCtx context.Context, wg *sync.WaitGroup) error { 111 | err := s.Consumer().RegisterNFInstance(s.CancelContext()) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | wg.Add(1) 117 | go s.startServer(wg) 118 | 119 | return nil 120 | } 121 | 122 | func (s *Server) Stop() { 123 | const defaultShutdownTimeout time.Duration = 2 * time.Second 124 | 125 | if s.httpServer != nil { 126 | logger.SBILog.Infof("Stop SBI server (listen on %s)", s.httpServer.Addr) 127 | toCtx, cancel := context.WithTimeout(context.Background(), defaultShutdownTimeout) 128 | defer cancel() 129 | if err := s.httpServer.Shutdown(toCtx); err != nil { 130 | logger.SBILog.Errorf("Could not close SBI server: %#v", err) 131 | } 132 | } 133 | } 134 | 135 | func (s *Server) startServer(wg *sync.WaitGroup) { 136 | defer func() { 137 | if p := recover(); p != nil { 138 | // Print stack for panic to log. Fatalf() will let program exit. 139 | logger.SBILog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 140 | } 141 | wg.Done() 142 | }() 143 | 144 | logger.SBILog.Infof("Start SBI server (listen on %s)", s.httpServer.Addr) 145 | 146 | var err error 147 | cfg := s.Config() 148 | scheme := cfg.GetSbiScheme() 149 | if scheme == "http" { 150 | err = s.httpServer.ListenAndServe() 151 | } else if scheme == "https" { 152 | err = s.httpServer.ListenAndServeTLS( 153 | cfg.GetCertPemPath(), 154 | cfg.GetCertKeyPath()) 155 | } else { 156 | err = fmt.Errorf("no support this scheme[%s]", scheme) 157 | } 158 | 159 | if err != nil && err != http.ErrServerClosed { 160 | logger.SBILog.Errorf("SBI server error: %v", err) 161 | } 162 | logger.SBILog.Infof("SBI server (listen on %s) stopped", s.httpServer.Addr) 163 | } 164 | -------------------------------------------------------------------------------- /internal/util/oauth/router_auth_check.go: -------------------------------------------------------------------------------- 1 | package util_oauth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/free5gc/openapi/models" 9 | smf_context "github.com/free5gc/smf/internal/context" 10 | "github.com/free5gc/smf/internal/logger" 11 | ) 12 | 13 | type RouterAuthorizationCheck struct { 14 | serviceName models.ServiceName 15 | } 16 | 17 | func NewRouterAuthorizationCheck(serviceName models.ServiceName) *RouterAuthorizationCheck { 18 | return &RouterAuthorizationCheck{ 19 | serviceName: serviceName, 20 | } 21 | } 22 | 23 | func (rac *RouterAuthorizationCheck) Check(c *gin.Context, smfContext smf_context.NFContext) { 24 | token := c.Request.Header.Get("Authorization") 25 | err := smfContext.AuthorizationCheck(token, rac.serviceName) 26 | if err != nil { 27 | logger.UtilLog.Debugf("RouterAuthorizationCheck: Check Unauthorized: %s", err.Error()) 28 | c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 29 | c.Abort() 30 | return 31 | } 32 | 33 | logger.UtilLog.Debugf("RouterAuthorizationCheck: Check Authorized") 34 | } 35 | -------------------------------------------------------------------------------- /internal/util/oauth/router_auth_check_test.go: -------------------------------------------------------------------------------- 1 | package util_oauth_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/free5gc/openapi/models" 12 | "github.com/free5gc/smf/internal/util/oauth" 13 | ) 14 | 15 | const ( 16 | Valid = "valid" 17 | Invalid = "invalid" 18 | ) 19 | 20 | type mockSMFContext struct{} 21 | 22 | func newMockSMFContext() *mockSMFContext { 23 | return &mockSMFContext{} 24 | } 25 | 26 | func (m *mockSMFContext) AuthorizationCheck(token string, serviceName models.ServiceName) error { 27 | if token == Valid { 28 | return nil 29 | } 30 | 31 | return errors.New("invalid token") 32 | } 33 | 34 | func TestRouterAuthorizationCheck_Check(t *testing.T) { 35 | // Mock gin.Context 36 | w := httptest.NewRecorder() 37 | c, _ := gin.CreateTestContext(w) 38 | 39 | var err error 40 | c.Request, err = http.NewRequest("GET", "/", nil) 41 | if err != nil { 42 | t.Errorf("error on http request: %+v", err) 43 | } 44 | 45 | type Args struct { 46 | token string 47 | } 48 | type Want struct { 49 | statusCode int 50 | } 51 | 52 | tests := []struct { 53 | name string 54 | args Args 55 | want Want 56 | }{ 57 | { 58 | name: "Valid Token", 59 | args: Args{ 60 | token: Valid, 61 | }, 62 | want: Want{ 63 | statusCode: http.StatusOK, 64 | }, 65 | }, 66 | { 67 | name: "Invalid Token", 68 | args: Args{ 69 | token: Invalid, 70 | }, 71 | want: Want{ 72 | statusCode: http.StatusUnauthorized, 73 | }, 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | w = httptest.NewRecorder() 80 | c, _ = gin.CreateTestContext(w) 81 | c.Request, err = http.NewRequest("GET", "/", nil) 82 | if err != nil { 83 | t.Errorf("error on http request: %+v", err) 84 | } 85 | c.Request.Header.Set("Authorization", tt.args.token) 86 | 87 | rac := util_oauth.NewRouterAuthorizationCheck(models.ServiceName("testService")) 88 | rac.Check(c, newMockSMFContext()) 89 | if w.Code != tt.want.statusCode { 90 | t.Errorf("StatusCode should be %d, but got %d", tt.want.statusCode, w.Code) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/util/qos_convert.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/free5gc/ngap/ngapType" 9 | ) 10 | 11 | func BitRateTokbps(bitrate string) (uint64, error) { 12 | s := strings.Split(bitrate, " ") 13 | var kbps uint64 14 | 15 | var digit int 16 | 17 | if n, err := strconv.Atoi(s[0]); err != nil { 18 | return 0, nil 19 | } else { 20 | digit = n 21 | } 22 | 23 | if len(s) == 1 { 24 | return 0, errors.New("cannot get the unit of ULMBR/DLMBR/ULGBR/DLGBR, please check the settings in web console") 25 | } 26 | 27 | switch s[1] { 28 | case "bps": 29 | kbps = uint64(digit / 1000) 30 | case "Kbps": 31 | kbps = uint64(digit * 1) 32 | case "Mbps": 33 | kbps = uint64(digit * 1000) 34 | case "Gbps": 35 | kbps = uint64(digit * 1000000) 36 | case "Tbps": 37 | kbps = uint64(digit * 1000000000) 38 | } 39 | return kbps, nil 40 | } 41 | 42 | func BitRateTombps(bitrate string) uint16 { 43 | s := strings.Split(bitrate, " ") 44 | var mbps uint16 45 | 46 | var digit int 47 | 48 | if n, err := strconv.Atoi(s[0]); err != nil { 49 | return 0 50 | } else { 51 | digit = n 52 | } 53 | 54 | switch s[1] { 55 | case "bps": 56 | mbps = uint16(digit / 1000000) 57 | case "Kbps": 58 | mbps = uint16(digit / 1000) 59 | case "Mbps": 60 | mbps = uint16(digit * 1) 61 | case "Gbps": 62 | mbps = uint16(digit * 1000) 63 | case "Tbps": 64 | mbps = uint16(digit * 1000000) 65 | } 66 | return mbps 67 | } 68 | 69 | func StringToBitRate(bitrate string) ngapType.BitRate { 70 | s := strings.Split(bitrate, " ") 71 | 72 | var digit int 73 | 74 | if n, err := strconv.Atoi(s[0]); err != nil { 75 | return ngapType.BitRate{Value: 0} 76 | } else { 77 | digit = n 78 | } 79 | switch s[1] { 80 | case "bps": 81 | // no need to modify 82 | case "Kbps": 83 | digit = (digit * 1000) 84 | case "Mbps": 85 | digit = (digit * 1000000) 86 | case "Gbps": 87 | digit = (digit * 1000000000) 88 | case "Tbps": 89 | digit = (digit * 1000000000000) 90 | } 91 | 92 | return ngapType.BitRate{Value: int64(digit)} 93 | } 94 | -------------------------------------------------------------------------------- /internal/util/qos_convert_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/free5gc/smf/internal/util" 7 | ) 8 | 9 | func TestBitRateToKbpsWithValidBpsBitRateShouldReturnValidKbpsBitRate(t *testing.T) { 10 | var bitrate string = "1000 bps" 11 | var correctBitRateKbps uint64 = 1 12 | 13 | bitrateKbps, err := util.BitRateTokbps(bitrate) 14 | 15 | t.Log("Check: err should be nil since act should work correctly.") 16 | if err != nil { 17 | t.Errorf("Error: err should be nil but it returns %s", err) 18 | } 19 | t.Log("Check: convert should act correctly.") 20 | if bitrateKbps != correctBitRateKbps { 21 | t.Errorf("Error: bitrate convert failed. Expect: %d. Actually: %d", correctBitRateKbps, bitrateKbps) 22 | } 23 | t.Log("Passed.") 24 | } 25 | 26 | func TestBitRateToKbpsWithValidKbpsBitRateShouldReturnValidKbpsBitRate(t *testing.T) { 27 | var bitrate string = "1000 Kbps" 28 | var correctBitRateKbps uint64 = 1000 29 | 30 | bitrateKbps, err := util.BitRateTokbps(bitrate) 31 | 32 | t.Log("Check: err should be nil since act should work correctly.") 33 | if err != nil { 34 | t.Errorf("Error: err should be nil but it returns %s", err) 35 | } 36 | t.Log("Check: convert should act correctly.") 37 | if bitrateKbps != correctBitRateKbps { 38 | t.Errorf("Error: bitrate convert failed. Expect: %d. Actually: %d", correctBitRateKbps, bitrateKbps) 39 | } 40 | t.Log("Passed.") 41 | } 42 | 43 | func TestBitRateToKbpsWithValidMbpsBitRateShouldReturnValidKbpsBitRate(t *testing.T) { 44 | var bitrate string = "1000 Mbps" 45 | var correctBitRateKbps uint64 = 1000000 46 | 47 | bitrateKbps, err := util.BitRateTokbps(bitrate) 48 | 49 | t.Log("Check: err should be nil since act should work correctly.") 50 | if err != nil { 51 | t.Errorf("Error: err should be nil but it returns %s", err) 52 | } 53 | t.Log("Check: convert should act correctly.") 54 | if bitrateKbps != correctBitRateKbps { 55 | t.Errorf("Error: bitrate convert failed. Expect: %d. Actually: %d", correctBitRateKbps, bitrateKbps) 56 | } 57 | t.Log("Passed.") 58 | } 59 | 60 | func TestBitRateToKbpsWithValidGbpsBitRateShouldReturnValidKbpsBitRate(t *testing.T) { 61 | var bitrate string = "1000 Gbps" 62 | var correctBitRateKbps uint64 = 1000000000 63 | 64 | bitrateKbps, err := util.BitRateTokbps(bitrate) 65 | 66 | t.Log("Check: err should be nil since act should work correctly.") 67 | if err != nil { 68 | t.Errorf("Error: err should be nil but it returns %s", err) 69 | } 70 | t.Log("Check: convert should act correctly.") 71 | if bitrateKbps != correctBitRateKbps { 72 | t.Errorf("Error: bitrate convert failed. Expect: %d. Actually: %d", correctBitRateKbps, bitrateKbps) 73 | } 74 | t.Log("Passed.") 75 | } 76 | 77 | func TestBitRateToKbpsWithValidTbpsBitRateShouldReturnValidKbpsBitRate(t *testing.T) { 78 | var bitrate string = "1000 Tbps" 79 | var correctBitRateKbps uint64 = 1000000000000 80 | 81 | bitrateKbps, err := util.BitRateTokbps(bitrate) 82 | 83 | t.Log("Check: err should be nil since act should work correctly.") 84 | if err != nil { 85 | t.Errorf("Error: err should be nil but it returns %s", err) 86 | } 87 | t.Log("Check: convert should act correctly.") 88 | if bitrateKbps != correctBitRateKbps { 89 | t.Errorf("Error: bitrate convert failed. Expect: %d. Actually: %d", correctBitRateKbps, bitrateKbps) 90 | } 91 | t.Log("Passed.") 92 | } 93 | 94 | func TestBitRateToKbpsWithInvalidBitRateShouldReturnError(t *testing.T) { 95 | var bitrate string = "1000" // The unit is absent. It should raise error for `BitRateToKbps`. 96 | 97 | _, err := util.BitRateTokbps(bitrate) 98 | 99 | t.Log("Check: err should not be nil.") 100 | if err == nil { 101 | t.Error("Error: err should not be nil.") 102 | } 103 | t.Log("Passed.") 104 | } 105 | -------------------------------------------------------------------------------- /internal/util/search_nf_service.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/free5gc/openapi/models" 7 | ) 8 | 9 | func SearchNFServiceUri(nfProfile *models.NrfNfDiscoveryNfProfile, serviceName models.ServiceName, 10 | nfServiceStatus models.NfServiceStatus, 11 | ) (nfUri string) { 12 | if nfProfile.NfServices != nil { 13 | for _, service := range nfProfile.NfServices { 14 | if service.ServiceName == serviceName && service.NfServiceStatus == nfServiceStatus { 15 | if nfProfile.Fqdn != "" { 16 | nfUri = nfProfile.Fqdn 17 | } else if service.Fqdn != "" { 18 | nfUri = service.Fqdn 19 | } else if service.ApiPrefix != "" { 20 | nfUri = service.ApiPrefix 21 | } else if service.IpEndPoints != nil { 22 | point := service.IpEndPoints[0] 23 | if point.Ipv4Address != "" { 24 | nfUri = getSbiUri(service.Scheme, point.Ipv4Address, point.Port) 25 | } else if len(nfProfile.Ipv4Addresses) != 0 { 26 | nfUri = getSbiUri(service.Scheme, nfProfile.Ipv4Addresses[0], point.Port) 27 | } 28 | } 29 | } 30 | if nfUri != "" { 31 | break 32 | } 33 | } 34 | } 35 | return 36 | } 37 | 38 | func getSbiUri(scheme models.UriScheme, ipv4Address string, port int32) (uri string) { 39 | if port != 0 { 40 | uri = fmt.Sprintf("%s://%s:%d", scheme, ipv4Address, port) 41 | } else { 42 | switch scheme { 43 | case models.UriScheme_HTTP: 44 | uri = fmt.Sprintf("%s://%s:80", scheme, ipv4Address) 45 | case models.UriScheme_HTTPS: 46 | uri = fmt.Sprintf("%s://%s:443", scheme, ipv4Address) 47 | } 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | smf_context "github.com/free5gc/smf/internal/context" 5 | "github.com/free5gc/smf/pkg/factory" 6 | ) 7 | 8 | type App interface { 9 | SetLogEnable(enable bool) 10 | SetLogLevel(level string) 11 | SetReportCaller(reportCaller bool) 12 | 13 | Start() 14 | Terminate() 15 | 16 | Context() *smf_context.SMFContext 17 | Config() *factory.Config 18 | } 19 | -------------------------------------------------------------------------------- /pkg/app/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/app/app.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -package=app -source=pkg/app/app.go 7 | // 8 | 9 | // Package app is a generated GoMock package. 10 | package app 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | context "github.com/free5gc/smf/internal/context" 16 | factory "github.com/free5gc/smf/pkg/factory" 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockApp is a mock of App interface. 21 | type MockApp struct { 22 | ctrl *gomock.Controller 23 | recorder *MockAppMockRecorder 24 | } 25 | 26 | // MockAppMockRecorder is the mock recorder for MockApp. 27 | type MockAppMockRecorder struct { 28 | mock *MockApp 29 | } 30 | 31 | // NewMockApp creates a new mock instance. 32 | func NewMockApp(ctrl *gomock.Controller) *MockApp { 33 | mock := &MockApp{ctrl: ctrl} 34 | mock.recorder = &MockAppMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockApp) EXPECT() *MockAppMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // Config mocks base method. 44 | func (m *MockApp) Config() *factory.Config { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "Config") 47 | ret0, _ := ret[0].(*factory.Config) 48 | return ret0 49 | } 50 | 51 | // Config indicates an expected call of Config. 52 | func (mr *MockAppMockRecorder) Config() *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockApp)(nil).Config)) 55 | } 56 | 57 | // Context mocks base method. 58 | func (m *MockApp) Context() *context.SMFContext { 59 | m.ctrl.T.Helper() 60 | ret := m.ctrl.Call(m, "Context") 61 | ret0, _ := ret[0].(*context.SMFContext) 62 | return ret0 63 | } 64 | 65 | // Context indicates an expected call of Context. 66 | func (mr *MockAppMockRecorder) Context() *gomock.Call { 67 | mr.mock.ctrl.T.Helper() 68 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockApp)(nil).Context)) 69 | } 70 | 71 | // SetLogEnable mocks base method. 72 | func (m *MockApp) SetLogEnable(enable bool) { 73 | m.ctrl.T.Helper() 74 | m.ctrl.Call(m, "SetLogEnable", enable) 75 | } 76 | 77 | // SetLogEnable indicates an expected call of SetLogEnable. 78 | func (mr *MockAppMockRecorder) SetLogEnable(enable any) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogEnable", reflect.TypeOf((*MockApp)(nil).SetLogEnable), enable) 81 | } 82 | 83 | // SetLogLevel mocks base method. 84 | func (m *MockApp) SetLogLevel(level string) { 85 | m.ctrl.T.Helper() 86 | m.ctrl.Call(m, "SetLogLevel", level) 87 | } 88 | 89 | // SetLogLevel indicates an expected call of SetLogLevel. 90 | func (mr *MockAppMockRecorder) SetLogLevel(level any) *gomock.Call { 91 | mr.mock.ctrl.T.Helper() 92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogLevel", reflect.TypeOf((*MockApp)(nil).SetLogLevel), level) 93 | } 94 | 95 | // SetReportCaller mocks base method. 96 | func (m *MockApp) SetReportCaller(reportCaller bool) { 97 | m.ctrl.T.Helper() 98 | m.ctrl.Call(m, "SetReportCaller", reportCaller) 99 | } 100 | 101 | // SetReportCaller indicates an expected call of SetReportCaller. 102 | func (mr *MockAppMockRecorder) SetReportCaller(reportCaller any) *gomock.Call { 103 | mr.mock.ctrl.T.Helper() 104 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReportCaller", reflect.TypeOf((*MockApp)(nil).SetReportCaller), reportCaller) 105 | } 106 | 107 | // Start mocks base method. 108 | func (m *MockApp) Start() { 109 | m.ctrl.T.Helper() 110 | m.ctrl.Call(m, "Start") 111 | } 112 | 113 | // Start indicates an expected call of Start. 114 | func (mr *MockAppMockRecorder) Start() *gomock.Call { 115 | mr.mock.ctrl.T.Helper() 116 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockApp)(nil).Start)) 117 | } 118 | 119 | // Terminate mocks base method. 120 | func (m *MockApp) Terminate() { 121 | m.ctrl.T.Helper() 122 | m.ctrl.Call(m, "Terminate") 123 | } 124 | 125 | // Terminate indicates an expected call of Terminate. 126 | func (mr *MockAppMockRecorder) Terminate() *gomock.Call { 127 | mr.mock.ctrl.T.Helper() 128 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockApp)(nil).Terminate)) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/free5gc/openapi/models" 7 | ) 8 | 9 | var ( 10 | N1SmError = models.SmfPduSessionExtProblemDetails{ 11 | Title: "Invalid N1 Message", 12 | Status: http.StatusForbidden, 13 | Detail: "N1 Message Error", 14 | Cause: "N1_SM_ERROR", 15 | } 16 | N2SmError = models.SmfPduSessionExtProblemDetails{ 17 | Title: "Invalid N2 Message", 18 | Status: http.StatusForbidden, 19 | Detail: "N2 Message Error", 20 | Cause: "N2_SM_ERROR", 21 | } 22 | DnnDeniedError = models.SmfPduSessionExtProblemDetails{ 23 | Title: "DNN Denied", 24 | Status: http.StatusForbidden, 25 | Detail: "The subscriber does not have the necessary subscription to access the DNN", 26 | Cause: "DNN_DENIED", 27 | InvalidParams: nil, 28 | } 29 | DnnNotSupported = models.SmfPduSessionExtProblemDetails{ 30 | Title: "DNN Not Supported", 31 | Status: http.StatusForbidden, 32 | Detail: "The DNN is not supported by the SMF.", 33 | Cause: "DNN_NOT_SUPPORTED", 34 | InvalidParams: nil, 35 | } 36 | InsufficientResourceSliceDnn = models.SmfPduSessionExtProblemDetails{ 37 | Title: "DNN Resource insufficient", 38 | Status: http.StatusInternalServerError, 39 | Detail: "The request cannot be provided due to insufficient resources for the specific slice and DNN.", 40 | Cause: "INSUFFICIENT_RESOURCES_SLICE_DNN", 41 | InvalidParams: nil, 42 | } 43 | SubscriptionDenied = models.SmfPduSessionExtProblemDetails{ 44 | Title: "Subscription Denied", 45 | Status: http.StatusForbidden, 46 | Detail: "This indicates an error, other than those listed in this table, " + 47 | "due to lack of necessary subscription to serve the UE request.", 48 | Cause: "SUBSCRIPTION_DENIED", 49 | InvalidParams: nil, 50 | } 51 | NetworkFailure = models.SmfPduSessionExtProblemDetails{ 52 | Title: "Network failure", 53 | Status: http.StatusGatewayTimeout, 54 | Detail: "The request is rejected due to a network problem.", 55 | Cause: "NETWORK_FAILURE", 56 | InvalidParams: nil, 57 | } 58 | SmContextStateMismatchActive = models.SmfPduSessionExtProblemDetails{ 59 | Title: "SMContext state mismatch", 60 | Status: http.StatusForbidden, 61 | Detail: "The SMContext State should be Active State.", 62 | } 63 | SmContextStateMismatchInActive = models.SmfPduSessionExtProblemDetails{ 64 | Title: "SMContext state mismatch", 65 | Status: http.StatusForbidden, 66 | Detail: "The SMContext State should be InActive State.", 67 | } 68 | ) 69 | -------------------------------------------------------------------------------- /pkg/factory/config_test.go: -------------------------------------------------------------------------------- 1 | package factory_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/free5gc/openapi/models" 9 | "github.com/free5gc/smf/pkg/factory" 10 | ) 11 | 12 | func TestSnssaiInfoItem(t *testing.T) { 13 | testcase := []struct { 14 | Name string 15 | Snssai *models.Snssai 16 | DnnInfos []*factory.SnssaiDnnInfoItem 17 | }{ 18 | { 19 | Name: "Default", 20 | Snssai: &models.Snssai{ 21 | Sst: int32(1), 22 | Sd: "010203", 23 | }, 24 | DnnInfos: []*factory.SnssaiDnnInfoItem{ 25 | { 26 | Dnn: "internet", 27 | DNS: &factory.DNS{ 28 | IPv4Addr: "8.8.8.8", 29 | }, 30 | }, 31 | }, 32 | }, 33 | { 34 | Name: "Empty SD", 35 | Snssai: &models.Snssai{ 36 | Sst: int32(1), 37 | }, 38 | DnnInfos: []*factory.SnssaiDnnInfoItem{ 39 | { 40 | Dnn: "internet2", 41 | DNS: &factory.DNS{ 42 | IPv4Addr: "1.1.1.1", 43 | }, 44 | }, 45 | }, 46 | }, 47 | } 48 | 49 | for _, tc := range testcase { 50 | t.Run(tc.Name, func(t *testing.T) { 51 | snssaiInfoItem := factory.SnssaiInfoItem{ 52 | SNssai: tc.Snssai, 53 | DnnInfos: tc.DnnInfos, 54 | } 55 | 56 | ok, err := snssaiInfoItem.Validate() 57 | require.True(t, ok) 58 | require.Nil(t, err) 59 | }) 60 | } 61 | } 62 | 63 | func TestSnssaiUpfInfoItem(t *testing.T) { 64 | testcase := []struct { 65 | Name string 66 | Snssai *models.Snssai 67 | DnnInfos []*factory.DnnUpfInfoItem 68 | }{ 69 | { 70 | Name: "Default", 71 | Snssai: &models.Snssai{ 72 | Sst: int32(1), 73 | Sd: "010203", 74 | }, 75 | DnnInfos: []*factory.DnnUpfInfoItem{ 76 | { 77 | Dnn: "internet", 78 | }, 79 | }, 80 | }, 81 | { 82 | Name: "Empty SD", 83 | Snssai: &models.Snssai{ 84 | Sst: int32(1), 85 | }, 86 | DnnInfos: []*factory.DnnUpfInfoItem{ 87 | { 88 | Dnn: "internet2", 89 | }, 90 | }, 91 | }, 92 | } 93 | 94 | for _, tc := range testcase { 95 | t.Run(tc.Name, func(t *testing.T) { 96 | snssaiInfoItem := factory.SnssaiUpfInfoItem{ 97 | SNssai: tc.Snssai, 98 | DnnUpfInfoList: tc.DnnInfos, 99 | } 100 | 101 | ok, err := snssaiInfoItem.Validate() 102 | require.True(t, ok) 103 | require.Nil(t, err) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pkg/factory/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SMF Configuration Factory 3 | */ 4 | 5 | package factory 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/asaskevich/govalidator" 12 | "gopkg.in/yaml.v2" 13 | 14 | "github.com/free5gc/smf/internal/logger" 15 | ) 16 | 17 | var ( 18 | SmfConfig *Config 19 | UERoutingConfig *RoutingConfig 20 | ) 21 | 22 | // TODO: Support configuration update from REST api 23 | func InitConfigFactory(f string, cfg *Config) error { 24 | if f == "" { 25 | // Use default config path 26 | f = SmfDefaultConfigPath 27 | } 28 | 29 | if content, err := os.ReadFile(f); err != nil { 30 | return fmt.Errorf("[Factory] %+v", err) 31 | } else { 32 | logger.CfgLog.Infof("Read config from [%s]", f) 33 | if yamlErr := yaml.Unmarshal(content, cfg); yamlErr != nil { 34 | return fmt.Errorf("[Factory] %+v", yamlErr) 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func InitRoutingConfigFactory(f string, cfg *RoutingConfig) error { 42 | if f == "" { 43 | // Use default config path 44 | f = SmfDefaultUERoutingPath 45 | } 46 | if content, err := os.ReadFile(f); err != nil { 47 | return err 48 | } else { 49 | logger.CfgLog.Infof("Read config from [%s]", f) 50 | if yamlErr := yaml.Unmarshal(content, cfg); yamlErr != nil { 51 | return fmt.Errorf("[Factory] %+v", yamlErr) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func ReadConfig(cfgPath string) (*Config, error) { 59 | cfg := &Config{} 60 | if err := InitConfigFactory(cfgPath, cfg); err != nil { 61 | return nil, fmt.Errorf("ReadConfig [%s] Error: %+v", cfgPath, err) 62 | } 63 | if _, err := cfg.Validate(); err != nil { 64 | validErrs := err.(govalidator.Errors).Errors() 65 | for _, validErr := range validErrs { 66 | logger.CfgLog.Errorf("%+v", validErr) 67 | } 68 | logger.CfgLog.Errorf("[-- PLEASE REFER TO SAMPLE CONFIG FILE COMMENTS --]") 69 | return nil, fmt.Errorf("Config validate Error") 70 | } 71 | 72 | return cfg, nil 73 | } 74 | 75 | func ReadUERoutingConfig(cfgPath string) (*RoutingConfig, error) { 76 | ueRoutingCfg := &RoutingConfig{} 77 | if err := InitRoutingConfigFactory(cfgPath, ueRoutingCfg); err != nil { 78 | return nil, fmt.Errorf("ReadConfig [%s] Error: %+v", cfgPath, err) 79 | } 80 | if _, err := ueRoutingCfg.Validate(); err != nil { 81 | validErrs := err.(govalidator.Errors).Errors() 82 | for _, validErr := range validErrs { 83 | logger.CfgLog.Errorf("%+v", validErr) 84 | } 85 | logger.CfgLog.Errorf("[-- PLEASE REFER TO SAMPLE CONFIG FILE COMMENTS --]") 86 | return nil, fmt.Errorf("Config validate Error") 87 | } 88 | 89 | return ueRoutingCfg, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "runtime/debug" 8 | "sync" 9 | 10 | "github.com/sirupsen/logrus" 11 | 12 | "github.com/free5gc/openapi" 13 | "github.com/free5gc/openapi/nrf/NFManagement" 14 | smf_context "github.com/free5gc/smf/internal/context" 15 | "github.com/free5gc/smf/internal/logger" 16 | "github.com/free5gc/smf/internal/sbi" 17 | "github.com/free5gc/smf/internal/sbi/consumer" 18 | "github.com/free5gc/smf/internal/sbi/processor" 19 | "github.com/free5gc/smf/pkg/app" 20 | "github.com/free5gc/smf/pkg/factory" 21 | ) 22 | 23 | type SmfAppInterface interface { 24 | app.App 25 | 26 | Consumer() *consumer.Consumer 27 | Processor() *processor.Processor 28 | } 29 | 30 | var SMF SmfAppInterface 31 | 32 | type SmfApp struct { 33 | SmfAppInterface 34 | 35 | cfg *factory.Config 36 | smfCtx *smf_context.SMFContext 37 | ctx context.Context 38 | cancel context.CancelFunc 39 | 40 | sbiServer *sbi.Server 41 | consumer *consumer.Consumer 42 | processor *processor.Processor 43 | wg sync.WaitGroup 44 | 45 | pfcpStart func(*SmfApp) 46 | pfcpTerminate func() 47 | } 48 | 49 | func GetApp() SmfAppInterface { 50 | return SMF 51 | } 52 | 53 | func NewApp( 54 | ctx context.Context, cfg *factory.Config, tlsKeyLogPath string, 55 | pfcpStart func(*SmfApp), pfcpTerminate func(), 56 | ) (*SmfApp, error) { 57 | smf_context.Init() 58 | smf := &SmfApp{ 59 | cfg: cfg, 60 | wg: sync.WaitGroup{}, 61 | pfcpStart: pfcpStart, 62 | pfcpTerminate: pfcpTerminate, 63 | smfCtx: smf_context.GetSelf(), 64 | } 65 | smf.SetLogEnable(cfg.GetLogEnable()) 66 | smf.SetLogLevel(cfg.GetLogLevel()) 67 | smf.SetReportCaller(cfg.GetLogReportCaller()) 68 | 69 | // Initialize consumer 70 | consumer, err := consumer.NewConsumer(smf) 71 | if err != nil { 72 | return nil, err 73 | } 74 | smf.consumer = consumer 75 | 76 | // Initialize processor 77 | processor, err := processor.NewProcessor(smf) 78 | if err != nil { 79 | return nil, err 80 | } 81 | smf.processor = processor 82 | 83 | // TODO: Initialize sbi server 84 | sbiServer, err := sbi.NewServer(smf, tlsKeyLogPath) 85 | if err != nil { 86 | return nil, err 87 | } 88 | smf.sbiServer = sbiServer 89 | 90 | smf.ctx, smf.cancel = context.WithCancel(ctx) 91 | 92 | // for PFCP 93 | smfContext := smf_context.GetSelf() 94 | smfContext.PfcpContext, smfContext.PfcpCancelFunc = context.WithCancel(smf.ctx) 95 | 96 | SMF = smf 97 | 98 | return smf, nil 99 | } 100 | 101 | func (a *SmfApp) Config() *factory.Config { 102 | return a.cfg 103 | } 104 | 105 | func (a *SmfApp) Context() *smf_context.SMFContext { 106 | return a.smfCtx 107 | } 108 | 109 | func (a *SmfApp) CancelContext() context.Context { 110 | return a.ctx 111 | } 112 | 113 | func (a *SmfApp) Consumer() *consumer.Consumer { 114 | return a.consumer 115 | } 116 | 117 | func (a *SmfApp) Processor() *processor.Processor { 118 | return a.processor 119 | } 120 | 121 | func (a *SmfApp) SetLogEnable(enable bool) { 122 | logger.MainLog.Infof("Log enable is set to [%v]", enable) 123 | if enable && logger.Log.Out == os.Stderr { 124 | return 125 | } else if !enable && logger.Log.Out == io.Discard { 126 | return 127 | } 128 | 129 | a.cfg.SetLogEnable(enable) 130 | if enable { 131 | logger.Log.SetOutput(os.Stderr) 132 | } else { 133 | logger.Log.SetOutput(io.Discard) 134 | } 135 | } 136 | 137 | func (a *SmfApp) SetLogLevel(level string) { 138 | lvl, err := logrus.ParseLevel(level) 139 | if err != nil { 140 | logger.MainLog.Warnf("Log level [%s] is invalid", level) 141 | return 142 | } 143 | 144 | logger.MainLog.Infof("Log level is set to [%s]", level) 145 | if lvl == logger.Log.GetLevel() { 146 | return 147 | } 148 | 149 | a.cfg.SetLogLevel(level) 150 | logger.Log.SetLevel(lvl) 151 | } 152 | 153 | func (a *SmfApp) SetReportCaller(reportCaller bool) { 154 | logger.MainLog.Infof("Report Caller is set to [%v]", reportCaller) 155 | if reportCaller == logger.Log.ReportCaller { 156 | return 157 | } 158 | 159 | a.cfg.SetLogReportCaller(reportCaller) 160 | logger.Log.SetReportCaller(reportCaller) 161 | } 162 | 163 | func (a *SmfApp) Start() { 164 | logger.InitLog.Infoln("Server started") 165 | 166 | err := a.sbiServer.Run(context.Background(), &a.wg) 167 | if err != nil { 168 | logger.MainLog.Errorf("sbi server run error %+v", err) 169 | } 170 | 171 | a.wg.Add(1) 172 | go a.listenShutDownEvent() 173 | 174 | // Initialize PFCP server 175 | a.pfcpStart(a) 176 | 177 | a.WaitRoutineStopped() 178 | } 179 | 180 | func (a *SmfApp) listenShutDownEvent() { 181 | defer func() { 182 | if p := recover(); p != nil { 183 | // Print stack for panic to log. Fatalf() will let program exit. 184 | logger.MainLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) 185 | } 186 | a.wg.Done() 187 | }() 188 | 189 | <-a.ctx.Done() 190 | a.terminateProcedure() 191 | } 192 | 193 | func (a *SmfApp) Terminate() { 194 | a.cancel() 195 | } 196 | 197 | func (a *SmfApp) terminateProcedure() { 198 | logger.MainLog.Infof("Terminating SMF...") 199 | a.pfcpTerminate() 200 | // deregister with NRF 201 | err := a.Consumer().SendDeregisterNFInstance() 202 | if err != nil { 203 | switch apiErr := err.(type) { 204 | case openapi.GenericOpenAPIError: 205 | switch errModel := apiErr.Model().(type) { 206 | case NFManagement.DeregisterNFInstanceError: 207 | pd := &errModel.ProblemDetails 208 | logger.MainLog.Errorf("Deregister NF instance Failed Problem[%+v]", pd) 209 | case error: 210 | logger.MainLog.Errorf("Deregister NF instance Error[%+v]", err) 211 | } 212 | case error: 213 | logger.MainLog.Errorf("Deregister NF instance Error[%+v]", err) 214 | } 215 | } else { 216 | logger.MainLog.Infof("Deregister from NRF successfully") 217 | } 218 | 219 | a.sbiServer.Stop() 220 | logger.MainLog.Infof("SMF SBI Server terminated") 221 | } 222 | 223 | func (a *SmfApp) WaitRoutineStopped() { 224 | a.wg.Wait() 225 | logger.MainLog.Infof("SMF App is terminated") 226 | } 227 | -------------------------------------------------------------------------------- /pkg/service/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/service/init.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -package=service -source=pkg/service/init.go 7 | // 8 | 9 | // Package service is a generated GoMock package. 10 | package service 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | context "github.com/free5gc/smf/internal/context" 16 | consumer "github.com/free5gc/smf/internal/sbi/consumer" 17 | processor "github.com/free5gc/smf/internal/sbi/processor" 18 | factory "github.com/free5gc/smf/pkg/factory" 19 | gomock "go.uber.org/mock/gomock" 20 | ) 21 | 22 | // MockSmfAppInterface is a mock of SmfAppInterface interface. 23 | type MockSmfAppInterface struct { 24 | ctrl *gomock.Controller 25 | recorder *MockSmfAppInterfaceMockRecorder 26 | } 27 | 28 | // MockSmfAppInterfaceMockRecorder is the mock recorder for MockSmfAppInterface. 29 | type MockSmfAppInterfaceMockRecorder struct { 30 | mock *MockSmfAppInterface 31 | } 32 | 33 | // NewMockSmfAppInterface creates a new mock instance. 34 | func NewMockSmfAppInterface(ctrl *gomock.Controller) *MockSmfAppInterface { 35 | mock := &MockSmfAppInterface{ctrl: ctrl} 36 | mock.recorder = &MockSmfAppInterfaceMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockSmfAppInterface) EXPECT() *MockSmfAppInterfaceMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // Config mocks base method. 46 | func (m *MockSmfAppInterface) Config() *factory.Config { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "Config") 49 | ret0, _ := ret[0].(*factory.Config) 50 | return ret0 51 | } 52 | 53 | // Config indicates an expected call of Config. 54 | func (mr *MockSmfAppInterfaceMockRecorder) Config() *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockSmfAppInterface)(nil).Config)) 57 | } 58 | 59 | // Consumer mocks base method. 60 | func (m *MockSmfAppInterface) Consumer() *consumer.Consumer { 61 | m.ctrl.T.Helper() 62 | ret := m.ctrl.Call(m, "Consumer") 63 | ret0, _ := ret[0].(*consumer.Consumer) 64 | return ret0 65 | } 66 | 67 | // Consumer indicates an expected call of Consumer. 68 | func (mr *MockSmfAppInterfaceMockRecorder) Consumer() *gomock.Call { 69 | mr.mock.ctrl.T.Helper() 70 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Consumer", reflect.TypeOf((*MockSmfAppInterface)(nil).Consumer)) 71 | } 72 | 73 | // Context mocks base method. 74 | func (m *MockSmfAppInterface) Context() *context.SMFContext { 75 | m.ctrl.T.Helper() 76 | ret := m.ctrl.Call(m, "Context") 77 | ret0, _ := ret[0].(*context.SMFContext) 78 | return ret0 79 | } 80 | 81 | // Context indicates an expected call of Context. 82 | func (mr *MockSmfAppInterfaceMockRecorder) Context() *gomock.Call { 83 | mr.mock.ctrl.T.Helper() 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockSmfAppInterface)(nil).Context)) 85 | } 86 | 87 | // Processor mocks base method. 88 | func (m *MockSmfAppInterface) Processor() *processor.Processor { 89 | m.ctrl.T.Helper() 90 | ret := m.ctrl.Call(m, "Processor") 91 | ret0, _ := ret[0].(*processor.Processor) 92 | return ret0 93 | } 94 | 95 | // Processor indicates an expected call of Processor. 96 | func (mr *MockSmfAppInterfaceMockRecorder) Processor() *gomock.Call { 97 | mr.mock.ctrl.T.Helper() 98 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Processor", reflect.TypeOf((*MockSmfAppInterface)(nil).Processor)) 99 | } 100 | 101 | // SetLogEnable mocks base method. 102 | func (m *MockSmfAppInterface) SetLogEnable(enable bool) { 103 | m.ctrl.T.Helper() 104 | m.ctrl.Call(m, "SetLogEnable", enable) 105 | } 106 | 107 | // SetLogEnable indicates an expected call of SetLogEnable. 108 | func (mr *MockSmfAppInterfaceMockRecorder) SetLogEnable(enable any) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogEnable", reflect.TypeOf((*MockSmfAppInterface)(nil).SetLogEnable), enable) 111 | } 112 | 113 | // SetLogLevel mocks base method. 114 | func (m *MockSmfAppInterface) SetLogLevel(level string) { 115 | m.ctrl.T.Helper() 116 | m.ctrl.Call(m, "SetLogLevel", level) 117 | } 118 | 119 | // SetLogLevel indicates an expected call of SetLogLevel. 120 | func (mr *MockSmfAppInterfaceMockRecorder) SetLogLevel(level any) *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogLevel", reflect.TypeOf((*MockSmfAppInterface)(nil).SetLogLevel), level) 123 | } 124 | 125 | // SetReportCaller mocks base method. 126 | func (m *MockSmfAppInterface) SetReportCaller(reportCaller bool) { 127 | m.ctrl.T.Helper() 128 | m.ctrl.Call(m, "SetReportCaller", reportCaller) 129 | } 130 | 131 | // SetReportCaller indicates an expected call of SetReportCaller. 132 | func (mr *MockSmfAppInterfaceMockRecorder) SetReportCaller(reportCaller any) *gomock.Call { 133 | mr.mock.ctrl.T.Helper() 134 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReportCaller", reflect.TypeOf((*MockSmfAppInterface)(nil).SetReportCaller), reportCaller) 135 | } 136 | 137 | // Start mocks base method. 138 | func (m *MockSmfAppInterface) Start() { 139 | m.ctrl.T.Helper() 140 | m.ctrl.Call(m, "Start") 141 | } 142 | 143 | // Start indicates an expected call of Start. 144 | func (mr *MockSmfAppInterfaceMockRecorder) Start() *gomock.Call { 145 | mr.mock.ctrl.T.Helper() 146 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSmfAppInterface)(nil).Start)) 147 | } 148 | 149 | // Terminate mocks base method. 150 | func (m *MockSmfAppInterface) Terminate() { 151 | m.ctrl.T.Helper() 152 | m.ctrl.Call(m, "Terminate") 153 | } 154 | 155 | // Terminate indicates an expected call of Terminate. 156 | func (mr *MockSmfAppInterfaceMockRecorder) Terminate() *gomock.Call { 157 | mr.mock.ctrl.T.Helper() 158 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockSmfAppInterface)(nil).Terminate)) 159 | } 160 | -------------------------------------------------------------------------------- /pkg/utils/pfcp_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | smf_context "github.com/free5gc/smf/internal/context" 8 | "github.com/free5gc/smf/internal/logger" 9 | "github.com/free5gc/smf/internal/pfcp" 10 | "github.com/free5gc/smf/internal/pfcp/udp" 11 | "github.com/free5gc/smf/pkg/service" 12 | ) 13 | 14 | func InitPFCPFunc(pCtx context.Context) (func(a *service.SmfApp), func()) { 15 | smfContext := smf_context.GetSelf() 16 | 17 | pfcpStart := func(a *service.SmfApp) { 18 | // Initialize PFCP server 19 | smfContext.PfcpContext, smfContext.PfcpCancelFunc = context.WithCancel(pCtx) 20 | 21 | udp.Run(pfcp.Dispatch) 22 | 23 | // Wait for PFCP start 24 | time.Sleep(1000 * time.Millisecond) 25 | 26 | for _, upNode := range smf_context.GetSelf().UserPlaneInformation.UPFs { 27 | go a.Processor().ToBeAssociatedWithUPF(smfContext.PfcpContext, upNode.UPF) 28 | } 29 | } 30 | 31 | pfcpStop := func() { 32 | smfContext.PfcpCancelFunc() 33 | err := udp.Server.Close() 34 | if err != nil { 35 | logger.Log.Errorf("udp server close failed %+v", err) 36 | } 37 | } 38 | 39 | return pfcpStart, pfcpStop 40 | } 41 | --------------------------------------------------------------------------------