From 2f3579b46ca250d20634e3ac01221ed8c378ccb2 Mon Sep 17 00:00:00 2001 From: Ian Chen Date: Thu, 28 Mar 2024 23:30:39 +0800 Subject: [PATCH] Feat/urr (#69) * support: URR related IEs * ignore session report msg in PeekNextResponse() * print log msg when received session report related msg * upgrade to golang 1.18 & go-pfcp v0.0.19 * update urr related handlers * support to handle sessionReport * add fuzz package * support ie fuzzing * update Fuzzing Test * ignore HeartbeatRequest * fix ci linter error * package rename * add license/copyright header * update makefile * Fix lint issue * fix gosec G404 & update behavior of test target in Makefile * update go.mod * fix ci-lint error in fuzz test * update README * fix ci linter error * fix: support loopback interface * Update .gitignore * fix: update typo and add args for fuzz test * doc: update README * Apply suggestions from code review --------- Co-authored-by: gab-arrobo Co-authored-by: CTFang@WireLab --- .gitignore | 3 + Makefile | 4 +- README.md | 80 +++++- fuzz/ie_fuzz_test.go | 94 +++++++ go.mod | 24 +- go.sum | 147 ++-------- internal/pfcpsim/export/export.go | 389 ++++++++++++++++++++++++++ internal/pfcpsim/helpers.go | 33 ++- internal/pfcpsim/helpers_test.go | 4 +- internal/pfcpsim/server.go | 75 ++++- internal/pfcpsim/state.go | 25 +- pkg/pfcpsim/pfcpsim.go | 186 +++++++++--- pkg/pfcpsim/session/far_builder.go | 31 +- pkg/pfcpsim/session/pdr_builder.go | 40 ++- pkg/pfcpsim/session/qer_builder.go | 47 +++- pkg/pfcpsim/session/urr_build_test.go | 121 ++++++++ pkg/pfcpsim/session/urr_builder.go | 159 +++++++++++ pkg/pfcpsim/session/urr_ies.go | 117 ++++++++ 18 files changed, 1363 insertions(+), 216 deletions(-) create mode 100644 fuzz/ie_fuzz_test.go create mode 100644 internal/pfcpsim/export/export.go create mode 100644 pkg/pfcpsim/session/urr_build_test.go create mode 100644 pkg/pfcpsim/session/urr_builder.go create mode 100644 pkg/pfcpsim/session/urr_ies.go diff --git a/.gitignore b/.gitignore index 25ea76a..f3d8e89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2022-present Open Networking Foundation +.fuzz/test_data .idea/ .coverage/ output *.pyc *.swp *.log +client +server diff --git a/Makefile b/Makefile index 2fed1b2..fdf887c 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,10 @@ golint: rm -rf $(CURDIR)/.coverage mkdir -p $(CURDIR)/.coverage +# -run flag ensures that the fuzz test won't be run +# because the fuzz test needs a UPF to run test: .coverage - go test -race -coverprofile=.coverage/coverage-unit.txt -covermode=atomic -v ./... + go test -race -coverprofile=.coverage/coverage-unit.txt -covermode=atomic -run=^Test -v ./... reuse-lint: docker run --rm -v $(CURDIR):/pfcpsim -w /pfcpsim omecproject/reuse-verify:latest reuse lint diff --git a/README.md b/README.md index b0f003e..a4ce302 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # pfcpsim pfcpsim is a simulator to interact with PFCP agents. Can be used to simulate a 4G SGW-C / 5G SMF. +> All related features are implemented according to the 3GPP TS 29.244 V16.3.1(2020-04). ## Overview pfcpsim is designed to work within a containerized environment. The docker image comes with both client (`pfcpctl`) and server (`pfcpsim`). @@ -13,7 +14,9 @@ pfcpsim is designed to work within a containerized environment. The docker image ## Getting Started -#### 1. Create the container. Images are available on [Dockerhub](https://hub.docker.com/r/opennetworking/pfcpsim/tags): +### Normal Case + +#### 1. Create the container. Images are available on [DockerHub](https://hub.docker.com/r/opennetworking/pfcpsim/tags): ```bash docker container run --rm -d --name pfcpsim pfcpsim: -p 12345 --interface ``` @@ -57,6 +60,81 @@ docker exec pfcpsim pfcpctl --server localhost:12345 session delete --count 5 -- docker exec pfcpsim pfcpctl --server localhost:12345 service disassociate ``` +### Fuzzing Mode + +Pfcpsim is able to generate malformed PFCP messages and can be used to explore potential vulnerabilities of PFCP agents (UPF). + +> Note: +> PFCP fuzzer is developed by the [Ian Chen (free5GC team)](https://github.com/ianchen0119) +> PFCP fuzzer was used to test the UPF implementation of the free5GC project, and successfully found some vulnerabilities. + +To use the PFCP fuzzer, we need to prepare the fuzzing environment first. The following steps show how to use the PFCP fuzzer. + +#### 1. Launch the UPF instance + +Pfcpsim supports to test various UPF implementations. +You can choose the UPF implementation you want to test, and launch the UPF instance. + +#### 2. Change the configuration in `fuzz/ie_fuzz_test.go` + +You should change the configuration in `fuzz/ie_fuzz_test.go`: +```go= +sim := export.NewPfcpSimCfg(iface, upfN3, upfN4) +``` +- `iface`: the interface name you used to establish the connection with UPF. +- `upfN3`: the N3 interface address of the UPF. +- `upfN4`: the N4 interface address of the UPF. + +#### 3. Run the fuzzing test + +You can run the fuzzing test by the following command: +``` +go test -fuzz=Fuzz -p 1 -parallel 1 -fuzztime 15m ./fuzz/... +``` +To specify args: +``` +go test -fuzz=Fuzz -p 1 -parallel 1 -fuzztime 15m ./fuzz/... -args -iface=lo -upfN3=192.168.0.5 -upfN4=127.0.0.8 +``` +- `-fuzztime`: the time you want to run the fuzzing test. +- Do not change the value of either `-parallel` or `-p` flag because it will cause the race condition. +- The output for the fuzzing test looks like this: +``` +fuzz: elapsed: 0s, gathering baseline coverage: 0/100 completed +fuzz: elapsed: 3s, gathering baseline coverage: 0/100 completed +... +fuzz: elapsed: 13m21s, gathering baseline coverage: 99/100 completed +fuzz: elapsed: 13m21s, gathering baseline coverage: 100/100 completed, now fuzzing with 1 workers +fuzz: elapsed: 13m24s, execs: 100 (0/sec), new interesting: 0 (total: 100) +... +fuzz: elapsed: 15m1s, execs: 111 (0/sec), new interesting: 0 (total: 100) +PASS +ok github.com/omec-project/pfcpsim/fuzz 900.684s +``` +- If the test result shows "PASS" and the UPF didn't crash, it means that the fuzzy test was successful! +- ``` + +- If Pfcpsim can't connect to the UPF, the user will see an output like this: +``` +... +failure while testing seed corpus entry: Fuzz/seed#0 +fuzz: elapsed: 5s, gathering baseline coverage: 0/106 completed +--- FAIL: Fuzz (5.02s) + --- FAIL: Fuzz (5.00s) + ie_fuzz_test.go:57: + Error Trace: /home/xxxx/pfcpsim/fuzz/ie_fuzz_test.go:57 + /usr/local/go/src/reflect/value.go:556 + /usr/local/go/src/reflect/value.go:339 + /usr/local/go/src/testing/fuzz.go:337 + Error: Received unexpected error: + route ip+net: no such network interface + Test: Fuzz + Messages: InitPFCPSim failed + +FAIL +exit status 1 +FAIL github.com/omec-project/pfcpsim/fuzz 5.023s +``` + ## Compile binaries If you don't want to use docker you can just compile the binaries of `pfcpsim` and `pfcpctl`: diff --git a/fuzz/ie_fuzz_test.go b/fuzz/ie_fuzz_test.go new file mode 100644 index 0000000..a9b03f9 --- /dev/null +++ b/fuzz/ie_fuzz_test.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024-present Ian Chen + +package fuzz + +import ( + "crypto/rand" + "flag" + "math/big" + "testing" + "time" + + "github.com/omec-project/pfcpsim/internal/pfcpsim/export" + "github.com/omec-project/pfcpsim/pkg/pfcpsim/session" + "github.com/stretchr/testify/require" +) + +const ( + MaxUint = ^uint(0) + MaxInt = int(MaxUint >> 1) +) + +func getRand(n int) int { + res, err := rand.Int(rand.Reader, big.NewInt(int64(n))) + if err != nil { + return n + } + + return int(res.Int64()) +} + +var ( + iface = flag.String("iface", "eth0", "the interface name you used to establish the connection with UPF.") + upfN3 = flag.String("upfN3", "192.168.0.5", "the N3 interface address of the UPF") + upfN4 = flag.String("upfN4", "127.0.0.8", "the N4 interface address of the UPF") +) + +func Fuzz(f *testing.F) { + var testcases []uint + for i := 0; i < 100; i++ { + testcases = append(testcases, uint(getRand(MaxInt))) + } + + for _, tc := range testcases { + f.Add(tc) + } + + session.SetCheck(false) + + f.Fuzz(func(t *testing.T, input uint) { + time.Sleep(5 * time.Second) + + sim := export.NewPfcpSimCfg(*iface, *upfN3, *upfN4) + + err := sim.InitPFCPSim() + if err != nil { + require.NoError(t, err, "InitPFCPSim failed") + } + + err = sim.Associate() + if err != nil { + require.NoError(t, err, "Associate failed") + } + + defer func() { + err = sim.TerminatePFCPSim() + require.NoError(t, err) + }() + + err = sim.CreateSession(2, getRand(session.PdrMax), + int(input)%session.QerMax, + int(input)%session.FarMax, + int(input)%session.UrrMax, + input) + if err != nil { + require.NoError(t, err, "CreateSession failed") + } + + err = sim.ModifySession(2, + getRand(session.FarMax), + getRand(session.UrrMax), + input) + if err != nil { + require.NoError(t, err, "ModifySession failed") + } + + time.Sleep(3 * time.Second) + + err = sim.DeleteSession(2) + if err != nil { + require.NoError(t, err, "DeleteSession failed") + } + }) +} diff --git a/go.mod b/go.mod index 55c9163..49521b2 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,23 @@ module github.com/omec-project/pfcpsim go 1.21 require ( - github.com/c-robinson/iplib v1.0.3 - github.com/golang/protobuf v1.5.2 + github.com/c-robinson/iplib v1.0.6 + github.com/golang/protobuf v1.5.3 github.com/jessevdk/go-flags v1.5.0 github.com/pborman/getopt/v2 v2.1.0 - github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.0 - github.com/wmnsk/go-pfcp v0.0.14 - google.golang.org/grpc v1.44.0 - google.golang.org/protobuf v1.27.1 + github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.9.0 + github.com/wmnsk/go-pfcp v0.0.19 + google.golang.org/grpc v1.54.0 + google.golang.org/protobuf v1.30.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect - golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect - golang.org/x/text v0.3.0 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a5ad52f..8a85263 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,14 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU= -github.com/c-robinson/iplib v1.0.3/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/c-robinson/iplib v1.0.6 h1:FfZV9BWNrah3BgLCFl5/nDXe4RbOi/C9n+DeXFOv5CQ= +github.com/c-robinson/iplib v1.0.6/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -56,87 +17,33 @@ github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmO github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/wmnsk/go-pfcp v0.0.14 h1:PMX7zKZYHaRM8qgjle45a/yis4q9hOAwRTfBMt+o7U4= -github.com/wmnsk/go-pfcp v0.0.14/go.mod h1:QKYWo1Wac4hc1Ut1YeaPKxcYUf8oHBZyqODJC6VAPBI= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wmnsk/go-pfcp v0.0.19 h1:jLZpmunrG2eJ+4Lr/HDq8j+/2Z1JEq5TwQoF/hklxAg= +github.com/wmnsk/go-pfcp v0.0.19/go.mod h1:GdZzvBajb3sNUP8ohAhXcVgzuckLPe/fVBJlzJSIqXs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/pfcpsim/export/export.go b/internal/pfcpsim/export/export.go new file mode 100644 index 0000000..0dad5b3 --- /dev/null +++ b/internal/pfcpsim/export/export.go @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024-present Ian Chen + +package export + +import ( + "errors" + "fmt" + "net" + "time" + + "github.com/c-robinson/iplib" + "github.com/omec-project/pfcpsim/internal/pfcpsim" + sim "github.com/omec-project/pfcpsim/pkg/pfcpsim" + "github.com/omec-project/pfcpsim/pkg/pfcpsim/session" + log "github.com/sirupsen/logrus" + ieLib "github.com/wmnsk/go-pfcp/ie" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + SVC_INIT = iota + SVC_CONNECTED + SVC_ASSOCIATED +) + +var ( + notConnected = errors.New("Not connected") + notAssociated = errors.New("Not associated") +) + +const SessionStep = 10 + +type PfcpSimCfg struct { + interfaceName string + upfN3 string + serverAddr string + state int + sim *sim.PFCPClient +} + +func NewPfcpSimCfg(iface, upfN3, serverAddr string) *PfcpSimCfg { + return &PfcpSimCfg{ + interfaceName: iface, + upfN3: upfN3, + serverAddr: serverAddr, + state: SVC_INIT, + sim: nil, + } +} + +func (c *PfcpSimCfg) InitPFCPSim() error { + pfcpsim.NewPFCPSimService(c.interfaceName) + pfcpsim.SetRemotePeer(c.serverAddr) + pfcpsim.SetUpfN3(c.upfN3) + + var err error + + if err = pfcpsim.ConnectPFCPSim(); err != nil { + return err + } else { + c.state = SVC_CONNECTED + c.sim = pfcpsim.GetSimulator() + } + + return nil +} + +func (c *PfcpSimCfg) TerminatePFCPSim() error { + return pfcpsim.DisconnectPFCPSim() +} + +func (c *PfcpSimCfg) Associate() error { + switch c.state { + case SVC_INIT: + return notConnected + case SVC_CONNECTED: + err := c.sim.SetupAssociation() + if err != nil { + return err + } + } + + c.state = SVC_ASSOCIATED + + return nil +} + +func (c *PfcpSimCfg) Deassociate() error { + switch c.state { + case SVC_INIT: + return notConnected + case SVC_CONNECTED: + return notAssociated + case SVC_ASSOCIATED: + err := c.sim.TeardownAssociation() + if err != nil { + return err + } + + c.sim.DisconnectN4() + } + + c.state = SVC_INIT + + return nil +} + +type SessionIEs struct{} + +func (c *PfcpSimCfg) CreateSession(baseID int, + pdrBuilder, qerBuilder, farBuilder, urrBuilder int, + fuzz uint, +) error { + uePool := "10.60.0.0/15" + + appFilters := []string{""} + count := 1 + + lastUEAddr, _, err := net.ParseCIDR(uePool) + if err != nil { + errMsg := fmt.Sprintf(" Could not parse Address Pool: %v", err) + log.Error(errMsg) + + return status.Error(codes.Aborted, errMsg) + } + + var qfi uint8 = 6 + + for i := baseID; i < (count*SessionStep + baseID); i = i + SessionStep { + // using variables to ease comprehension on how rules are linked together + uplinkTEID := uint32(i) + + ueAddress := iplib.NextIP(lastUEAddr) + lastUEAddr = ueAddress + + sessQerID := uint32(0) + + var pdrs, fars, urrs []*ieLib.IE + + qers := []*ieLib.IE{ + // session QER + session.NewQERBuilder(). + WithID(sessQerID). + WithMethod(session.Create). + WithUplinkMBR(60000). + WithDownlinkMBR(60000). + FuzzIE(qerBuilder, fuzz). + Build(), + } + + // create as many PDRs, FARs and App QERs as the number of app filters provided through pfcpctl + ID := uint16(i) + + for _, appFilter := range appFilters { + SDFFilter, gateStatus, precedence, err := pfcpsim.ParseAppFilter(appFilter) + if err != nil { + return status.Error(codes.Aborted, err.Error()) + } + + log.Infof("Successfully parsed application filter. SDF Filter: %v", SDFFilter) + + uplinkPdrID := ID + downlinkPdrID := ID + 1 + + uplinkFarID := uint32(ID) + downlinkFarID := uint32(ID + 1) + + uplinkAppQerID := uint32(ID) + downlinkAppQerID := uint32(ID + 1) + + urrId := uint32(ID) + urr := session.NewURRBuilder(). + WithID(urrId). + WithMethod(session.Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(1*time.Second). + WithReportingTrigger(session.ReportingTrigger{ + Flags: session.RPT_TRIG_PERIO, + }). + FuzzIE(urrBuilder, fuzz). + Build() + + urrs = append(urrs, urr) + + urr = session.NewURRBuilder(). + WithID(urrId+1). + WithMethod(session.Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(1*time.Second). + WithReportingTrigger(session.ReportingTrigger{ + Flags: session.RPT_TRIG_VOLTH | session.RPT_TRIG_VOLQU, + }). + WithVolumeThreshold(7, 10000, 20000, 30000). + WithVolumeQuota(7, 10000, 20000, 30000). + FuzzIE(urrBuilder, fuzz). + Build() + + urrs = append(urrs, urr) + + uplinkPDR := session.NewPDRBuilder(). + WithID(uplinkPdrID). + WithMethod(session.Create). + WithTEID(uplinkTEID). + WithFARID(uplinkFarID). + AddQERID(sessQerID). + AddQERID(uplinkAppQerID). + WithN3Address(c.upfN3). + WithSDFFilter(SDFFilter). + WithPrecedence(precedence). + MarkAsUplink(). + FuzzIE(pdrBuilder, fuzz). + BuildPDR() + + downlinkPDR := session.NewPDRBuilder(). + WithID(downlinkPdrID). + WithMethod(session.Create). + WithPrecedence(precedence). + WithUEAddress(ueAddress.String()). + WithSDFFilter(SDFFilter). + AddQERID(sessQerID). + AddQERID(downlinkAppQerID). + WithFARID(downlinkFarID). + MarkAsDownlink(). + FuzzIE(pdrBuilder, fuzz). + BuildPDR() + + pdrs = append(pdrs, uplinkPDR) + pdrs = append(pdrs, downlinkPDR) + + uplinkFAR := session.NewFARBuilder(). + WithID(uplinkFarID). + WithAction(session.ActionForward). + WithDstInterface(ieLib.DstInterfaceCore). + WithMethod(session.Create). + FuzzIE(farBuilder, fuzz). + BuildFAR() + + downlinkFAR := session.NewFARBuilder(). + WithID(downlinkFarID). + WithAction(session.ActionDrop). + WithMethod(session.Create). + WithDstInterface(ieLib.DstInterfaceAccess). + WithZeroBasedOuterHeaderCreation(). + FuzzIE(farBuilder, fuzz). + BuildFAR() + + fars = append(fars, uplinkFAR) + fars = append(fars, downlinkFAR) + + uplinkAppQER := session.NewQERBuilder(). + WithID(uplinkAppQerID). + WithMethod(session.Create). + WithQFI(qfi). + WithUplinkMBR(50000). + WithDownlinkMBR(30000). + WithGateStatus(gateStatus). + FuzzIE(qerBuilder, fuzz). + Build() + + downlinkAppQER := session.NewQERBuilder(). + WithID(downlinkAppQerID). + WithMethod(session.Create). + WithQFI(qfi). + WithUplinkMBR(50000). + WithDownlinkMBR(30000). + WithGateStatus(gateStatus). + FuzzIE(qerBuilder, fuzz). + Build() + + qers = append(qers, uplinkAppQER) + qers = append(qers, downlinkAppQER) + + ID += 2 + } + + sess, err := c.sim.EstablishSession(pdrs, fars, qers, urrs) + if err != nil { + return status.Error(codes.Internal, err.Error()) + } + + sim.InsertSession(i, sess) + } + + infoMsg := fmt.Sprintf("%v sessions were established using %v as baseID ", count, baseID) + log.Info(infoMsg) + + return nil +} + +func (c *PfcpSimCfg) ModifySession(baseID int, + farBuilder, urrBuilder int, + fuzz uint, +) error { + var actions uint8 + + actions |= session.ActionNotify + count := 1 + nodeBaddress := "192.168.0.1" + appFilters := []string{""} + + for i := baseID; i < (count*SessionStep + baseID); i = i + SessionStep { + var newFARs []*ieLib.IE + + var newURRs []*ieLib.IE + + ID := uint32(i + 1) + teid := uint32(i + 1) + + for j := 0; j < len(appFilters); j++ { + downlinkFAR := session.NewFARBuilder(). + WithID(ID). // Same FARID that was generated in create sessions + WithMethod(session.Update). + WithAction(actions). + WithDstInterface(ieLib.DstInterfaceAccess). + WithTEID(teid). + WithDownlinkIP(nodeBaddress). + FuzzIE(farBuilder, fuzz). + BuildFAR() + + newFARs = append(newFARs, downlinkFAR) + + urrId := ID + urr := session.NewURRBuilder(). + WithID(urrId). + WithMethod(session.Update). + WithMeasurementPeriod(1*time.Second). + FuzzIE(urrBuilder, fuzz). + Build() + + newURRs = append(newURRs, urr) + + ID += 2 + } + + sess, ok := sim.GetSession(i) + if !ok { + errMsg := fmt.Sprintf("Could not retrieve session with index %v", i) + log.Error(errMsg) + + return status.Error(codes.Internal, errMsg) + } + + err := c.sim.ModifySession(sess, nil, newFARs, nil, newURRs) + if err != nil { + return status.Error(codes.Internal, err.Error()) + } + } + + infoMsg := fmt.Sprintf("%v sessions were modified", count) + log.Info(infoMsg) + + return nil +} + +func (c *PfcpSimCfg) DeleteSession(baseID int) error { + count := 1 + + if sim.GetActiveSessionNum() < count { + err := sim.NewNotEnoughSessionsError() + log.Error(err) + + return status.Error(codes.Aborted, err.Error()) + } + + for i := baseID; i < (count*SessionStep + baseID); i = i + SessionStep { + sess, ok := sim.GetSession(i) + if !ok { + errMsg := "Session was nil. Check baseID" + log.Error(errMsg) + + return status.Error(codes.Aborted, errMsg) + } + + err := c.sim.DeleteSession(sess) + if err != nil { + log.Error(err.Error()) + return status.Error(codes.Aborted, err.Error()) + } + // remove from activeSessions + sim.RemoveSession(i) + } + + infoMsg := fmt.Sprintf("%v sessions deleted; activeSessions: %v", count, sim.GetActiveSessionNum()) + log.Info(infoMsg) + + return nil +} diff --git a/internal/pfcpsim/helpers.go b/internal/pfcpsim/helpers.go index ae58011..892c05d 100644 --- a/internal/pfcpsim/helpers.go +++ b/internal/pfcpsim/helpers.go @@ -4,6 +4,8 @@ package pfcpsim import ( + "context" + "errors" "fmt" "net" "strconv" @@ -16,12 +18,18 @@ import ( "google.golang.org/grpc/status" ) +var notInit = errors.New("PFCP simulator is not initialized") + const ( sdfFilterFormatWPort = "permit out %v from %v to assigned %v-%v" sdfFilterFormatWOPort = "permit out %v from %v to assigned" ) -func connectPFCPSim() error { +func GetSimulator() *pfcpsim.PFCPClient { + return sim +} + +func ConnectPFCPSim() error { if sim == nil { localAddr, err := getLocalAddress(interfaceName) if err != nil { @@ -31,16 +39,30 @@ func connectPFCPSim() error { sim = pfcpsim.NewPFCPClient(localAddr.String()) } - err := sim.ConnectN4(remotePeerAddress) + ctx, cancel := context.WithCancel(context.Background()) + + err := sim.ConnectN4(ctx, remotePeerAddress) if err != nil { + cancel() return err } + cancelFunc = cancel remotePeerConnected = true return nil } +func DisconnectPFCPSim() error { + if sim == nil { + return notInit + } + + cancelFunc() + + return nil +} + func isConfigured() bool { if upfN3Address != "" && remotePeerAddress != "" { return true @@ -86,8 +108,7 @@ func getLocalAddress(interfaceName string) (net.IP, error) { } for _, address := range addrs { - // Check address type to be non-loopback - if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet, ok := address.(*net.IPNet); ok { if ipnet.IP.To4() != nil { return ipnet.IP, nil } @@ -97,9 +118,9 @@ func getLocalAddress(interfaceName string) (net.IP, error) { return nil, pfcpsim.NewNoValidInterfaceError() } -// parseAppFilter parses an application filter. Returns a tuple formed by a formatted SDF filter +// ParseAppFilter parses an application filter. Returns a tuple formed by a formatted SDF filter // and a uint8 representing the Application QER gate status and a precedence. Returns error if fail occurs while validating the filter string. -func parseAppFilter(filter string) (string, uint8, uint32, error) { +func ParseAppFilter(filter string) (string, uint8, uint32, error) { if filter == "" { // parsing a wildcard app filter return "", ie.GateStatusOpen, 100, nil diff --git a/internal/pfcpsim/helpers_test.go b/internal/pfcpsim/helpers_test.go index ab57993..d8f7b06 100644 --- a/internal/pfcpsim/helpers_test.go +++ b/internal/pfcpsim/helpers_test.go @@ -10,7 +10,7 @@ import ( "github.com/wmnsk/go-pfcp/ie" ) -func Test_parseAppFilter(t *testing.T) { +func Test_ParseAppFilter(t *testing.T) { type args struct { filterString string } @@ -130,7 +130,7 @@ func Test_parseAppFilter(t *testing.T) { for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { - filter, gateStatus, precedence, err := parseAppFilter(tt.args.filterString) + filter, gateStatus, precedence, err := ParseAppFilter(tt.args.filterString) if tt.wantErr { require.Error(t, err) return diff --git a/internal/pfcpsim/server.go b/internal/pfcpsim/server.go index f996403..0c6523e 100644 --- a/internal/pfcpsim/server.go +++ b/internal/pfcpsim/server.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net" + "time" "github.com/c-robinson/iplib" pb "github.com/omec-project/pfcpsim/api" @@ -45,6 +46,14 @@ func checkServerStatus() error { return nil } +func SetRemotePeer(addr string) { + remotePeerAddress = addr +} + +func SetUpfN3(addr string) { + upfN3Address = addr +} + func (P pfcpSimService) Configure(ctx context.Context, request *pb.ConfigureRequest) (*pb.Response, error) { if net.ParseIP(request.UpfN3Address) == nil { errMsg := fmt.Sprintf("Error while parsing UPF N3 address: %v", request.UpfN3Address) @@ -53,8 +62,8 @@ func (P pfcpSimService) Configure(ctx context.Context, request *pb.ConfigureRequ return &pb.Response{}, status.Error(codes.Aborted, errMsg) } // remotePeerAddress is validated in pfcpsim - remotePeerAddress = request.RemotePeerAddress - upfN3Address = request.UpfN3Address + SetRemotePeer(request.RemotePeerAddress) + SetUpfN3(request.UpfN3Address) configurationMsg := fmt.Sprintf("Server is configured. Remote peer address: %v, N3 interface address: %v ", remotePeerAddress, upfN3Address) log.Info(configurationMsg) @@ -72,7 +81,7 @@ func (P pfcpSimService) Associate(ctx context.Context, empty *pb.EmptyRequest) ( } if !isRemotePeerConnected() { - if err := connectPFCPSim(); err != nil { + if err := ConnectPFCPSim(); err != nil { errMsg := fmt.Sprintf("Could not connect to remote peer :%v", err) log.Error(errMsg) @@ -152,7 +161,7 @@ func (P pfcpSimService) CreateSession(ctx context.Context, request *pb.CreateSes sessQerID := uint32(0) - var pdrs, fars []*ieLib.IE + var pdrs, fars, urrs []*ieLib.IE qers := []*ieLib.IE{ // session QER @@ -168,7 +177,7 @@ func (P pfcpSimService) CreateSession(ctx context.Context, request *pb.CreateSes ID := uint16(i) for _, appFilter := range request.AppFilters { - SDFFilter, gateStatus, precedence, err := parseAppFilter(appFilter) + SDFFilter, gateStatus, precedence, err := ParseAppFilter(appFilter) if err != nil { return &pb.Response{}, status.Error(codes.Aborted, err.Error()) } @@ -184,6 +193,33 @@ func (P pfcpSimService) CreateSession(ctx context.Context, request *pb.CreateSes uplinkAppQerID := uint32(ID) downlinkAppQerID := uint32(ID + 1) + urrId := uint32(ID) + urr := session.NewURRBuilder(). + WithID(urrId). + WithMethod(session.Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(1 * time.Second). + WithReportingTrigger(session.ReportingTrigger{ + Flags: session.RPT_TRIG_PERIO, + }). + Build() + + urrs = append(urrs, urr) + + urr = session.NewURRBuilder(). + WithID(urrId+1). + WithMethod(session.Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(1*time.Second). + WithReportingTrigger(session.ReportingTrigger{ + Flags: session.RPT_TRIG_VOLTH | session.RPT_TRIG_VOLQU, + }). + WithVolumeThreshold(7, 10000, 20000, 30000). + WithVolumeQuota(7, 10000, 20000, 30000). + Build() + + urrs = append(urrs, urr) + uplinkPDR := session.NewPDRBuilder(). WithID(uplinkPdrID). WithMethod(session.Create). @@ -254,12 +290,12 @@ func (P pfcpSimService) CreateSession(ctx context.Context, request *pb.CreateSes ID += 2 } - sess, err := sim.EstablishSession(pdrs, fars, qers) + sess, err := sim.EstablishSession(pdrs, fars, qers, urrs) if err != nil { return &pb.Response{}, status.Error(codes.Internal, err.Error()) } - insertSession(i, sess) + pfcpsim.InsertSession(i, sess) } infoMsg := fmt.Sprintf("%v sessions were established using %v as baseID ", count, baseID) @@ -281,7 +317,7 @@ func (P pfcpSimService) ModifySession(ctx context.Context, request *pb.ModifySes count := int(request.Count) nodeBaddress := request.NodeBAddress - if len(activeSessions) < count { + if pfcpsim.GetActiveSessionNum() < count { err := pfcpsim.NewNotEnoughSessionsError() log.Error(err) @@ -306,6 +342,8 @@ func (P pfcpSimService) ModifySession(ctx context.Context, request *pb.ModifySes for i := baseID; i < (count*SessionStep + baseID); i = i + SessionStep { var newFARs []*ieLib.IE + var newURRs []*ieLib.IE + ID := uint32(i + 1) teid := uint32(i + 1) @@ -325,10 +363,19 @@ func (P pfcpSimService) ModifySession(ctx context.Context, request *pb.ModifySes newFARs = append(newFARs, downlinkFAR) + urrId := ID + urr := session.NewURRBuilder(). + WithID(urrId). + WithMethod(session.Update). + WithMeasurementPeriod(1 * time.Second). + Build() + + newURRs = append(newURRs, urr) + ID += 2 } - sess, ok := getSession(i) + sess, ok := pfcpsim.GetSession(i) if !ok { errMsg := fmt.Sprintf("Could not retrieve session with index %v", i) log.Error(errMsg) @@ -336,7 +383,7 @@ func (P pfcpSimService) ModifySession(ctx context.Context, request *pb.ModifySes return &pb.Response{}, status.Error(codes.Internal, errMsg) } - err := sim.ModifySession(sess, nil, newFARs, nil) + err := sim.ModifySession(sess, nil, newFARs, nil, newURRs) if err != nil { return &pb.Response{}, status.Error(codes.Internal, err.Error()) } @@ -359,7 +406,7 @@ func (P pfcpSimService) DeleteSession(ctx context.Context, request *pb.DeleteSes baseID := int(request.BaseID) count := int(request.Count) - if len(activeSessions) < count { + if pfcpsim.GetActiveSessionNum() < count { err := pfcpsim.NewNotEnoughSessionsError() log.Error(err) @@ -367,7 +414,7 @@ func (P pfcpSimService) DeleteSession(ctx context.Context, request *pb.DeleteSes } for i := baseID; i < (count*SessionStep + baseID); i = i + SessionStep { - sess, ok := getSession(i) + sess, ok := pfcpsim.GetSession(i) if !ok { errMsg := "Session was nil. Check baseID" log.Error(errMsg) @@ -381,10 +428,10 @@ func (P pfcpSimService) DeleteSession(ctx context.Context, request *pb.DeleteSes return &pb.Response{}, status.Error(codes.Aborted, err.Error()) } // remove from activeSessions - deleteSession(i) + pfcpsim.RemoveSession(i) } - infoMsg := fmt.Sprintf("%v sessions deleted; activeSessions: %v", count, len(activeSessions)) + infoMsg := fmt.Sprintf("%v sessions deleted; activeSessions: %v", count, pfcpsim.GetActiveSessionNum()) log.Info(infoMsg) return &pb.Response{ diff --git a/internal/pfcpsim/state.go b/internal/pfcpsim/state.go index 7493829..34f5d08 100644 --- a/internal/pfcpsim/state.go +++ b/internal/pfcpsim/state.go @@ -4,15 +4,12 @@ package pfcpsim import ( - "sync" + "context" "github.com/omec-project/pfcpsim/pkg/pfcpsim" ) var ( - activeSessions = make(map[int]*pfcpsim.PFCPSession, 0) - lockActiveSessions = new(sync.Mutex) - remotePeerAddress string upfN3Address string @@ -21,23 +18,5 @@ var ( // Emulates 5G SMF/ 4G SGW sim *pfcpsim.PFCPClient remotePeerConnected bool + cancelFunc context.CancelFunc ) - -func insertSession(index int, session *pfcpsim.PFCPSession) { - lockActiveSessions.Lock() - defer lockActiveSessions.Unlock() - - activeSessions[index] = session -} - -func getSession(index int) (*pfcpsim.PFCPSession, bool) { - element, ok := activeSessions[index] - return element, ok -} - -func deleteSession(index int) { - lockActiveSessions.Lock() - defer lockActiveSessions.Unlock() - - delete(activeSessions, index) -} diff --git a/pkg/pfcpsim/pfcpsim.go b/pkg/pfcpsim/pfcpsim.go index b7cb0e2..4a87ee2 100644 --- a/pkg/pfcpsim/pfcpsim.go +++ b/pkg/pfcpsim/pfcpsim.go @@ -5,11 +5,13 @@ package pfcpsim import ( "context" + "errors" "fmt" "net" "sync" "time" + "github.com/wmnsk/go-pfcp/ie" ieLib "github.com/wmnsk/go-pfcp/ie" "github.com/wmnsk/go-pfcp/message" ) @@ -20,6 +22,56 @@ const ( DefaultResponseTimeout = 5 * time.Second ) +var ( + activeSessions = make(map[int]*PFCPSession, 0) + lockActiveSessions = new(sync.Mutex) + wrongRspType = errors.New("unexpected response type") + assocFailed = errors.New("association failed") +) + +func GetActiveSessionNum() int { + lockActiveSessions.Lock() + defer lockActiveSessions.Unlock() + + return len(activeSessions) +} + +func InsertSession(index int, session *PFCPSession) { + lockActiveSessions.Lock() + defer lockActiveSessions.Unlock() + + activeSessions[index] = session +} + +func GetSession(index int) (*PFCPSession, bool) { + lockActiveSessions.Lock() + defer lockActiveSessions.Unlock() + + element, ok := activeSessions[index] + + return element, ok +} + +func GetSessionByLocalSEID(seid uint64) (*PFCPSession, bool) { + lockActiveSessions.Lock() + defer lockActiveSessions.Unlock() + + for _, session := range activeSessions { + if session.localSEID == seid { + return session, true + } + } + + return nil, false +} + +func RemoveSession(index int) { + lockActiveSessions.Lock() + defer lockActiveSessions.Unlock() + + delete(activeSessions, index) +} + // PFCPClient enables to simulate a client sending PFCP messages towards the UPF. // It provides two usage modes: // - 1st mode enables high-level PFCP operations (e.g., SetupAssociation()) @@ -42,8 +94,9 @@ type PFCPClient struct { sequenceNumber uint32 seqNumLock sync.Mutex - localAddr string - conn *net.UDPConn + localAddr string + remoteAddr string + conn *net.UDPConn // responseTimeout timeout to wait for PFCP response (default: 5 seconds) responseTimeout time.Duration @@ -101,40 +154,63 @@ func (c *PFCPClient) sendMsg(msg message.Message) error { return err } - if _, err := c.conn.Write(b); err != nil { + raddr, err := net.ResolveUDPAddr("udp", c.remoteAddr) + if err != nil { + return err + } + + if _, err := c.conn.WriteTo(b, raddr); err != nil { return err } return nil } -func (c *PFCPClient) receiveFromN4() { - buf := make([]byte, 1500) +func (c *PFCPClient) receiveFromN4(ctx context.Context) { + buf := make([]byte, 3000) for { - n, _, err := c.conn.ReadFrom(buf) - if err != nil { - continue - } - - msg, err := message.Parse(buf[:n]) - if err != nil { - continue - } + select { + case <-ctx.Done(): + if c.cancelHeartbeats != nil { + c.cancelHeartbeats() + } - switch msg := msg.(type) { - case *message.HeartbeatResponse: - c.heartbeatsChan <- msg + err := c.conn.Close() + if err != nil { + fmt.Println(err) + } - case *message.SessionReportRequest: - // Ignore message + return default: - c.recvChan <- msg + n, _, err := c.conn.ReadFrom(buf) + if err != nil { + continue + } + + msg, err := message.Parse(buf[:n]) + if err != nil { + continue + } + + switch msg := msg.(type) { + case *message.HeartbeatResponse: + c.heartbeatsChan <- msg + case *message.HeartbeatRequest: + // ignore HeartbeatRequest + continue + case *message.SessionReportRequest: + if c.handleSessionReportRequest(msg) { + continue + } + default: + c.recvChan <- msg + } } } } -func (c *PFCPClient) ConnectN4(remoteAddr string) error { +func (c *PFCPClient) ConnectN4(ctx context.Context, remoteAddr string) error { addr := fmt.Sprintf("%s:%d", remoteAddr, PFCPStandardPort) if host, port, err := net.SplitHostPort(remoteAddr); err == nil { @@ -142,19 +218,21 @@ func (c *PFCPClient) ConnectN4(remoteAddr string) error { addr = fmt.Sprintf("%s:%s", host, port) } - raddr, err := net.ResolveUDPAddr("udp", addr) + c.remoteAddr = addr + + laddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", c.localAddr, PFCPStandardPort)) if err != nil { return err } - conn, err := net.DialUDP("udp", nil, raddr) + rxconn, err := net.ListenUDP("udp4", laddr) if err != nil { return err } - c.conn = conn + c.conn = rxconn - go c.receiveFromN4() + go c.receiveFromN4(ctx) return nil } @@ -201,6 +279,40 @@ func (c *PFCPClient) PeekNextResponse() (message.Message, error) { } } +// MsgTypeSessionReportRequest: sent by the UP function to the CP function to report information related to an PFCP session +// MsgTypeSessionReportResponse: sent by the CP function to the UP function as a reply to the Session Report Request. +func (c *PFCPClient) handleSessionReportRequest(msg *message.SessionReportRequest) bool { + if msg.MessageType() == message.MsgTypeSessionReportRequest { + fmt.Println("Session Report Request received") + + err := c.sendSessionReportResponse(msg.Sequence(), + msg.Header.SEID) + if err != nil { + fmt.Println("Error sending Session Report Response") + } + + return true + } + + return false +} + +func (c *PFCPClient) sendSessionReportResponse(seq uint32, seid uint64) error { + var rseid uint64 + + sess, ok := GetSessionByLocalSEID(seid) + if !ok { + rseid = 0 + } else { + rseid = sess.peerSEID + } + + res := message.NewSessionReportResponse(0, 0, rseid, seq, 0, + ie.NewCause(ie.CauseRequestAccepted)) + + return c.sendMsg(res) +} + func (c *PFCPClient) SendAssociationSetupRequest(ie ...*ieLib.IE) error { c.resetSequenceNumber() @@ -237,7 +349,9 @@ func (c *PFCPClient) SendHeartbeatRequest() error { return c.sendMsg(hbReq) } -func (c *PFCPClient) SendSessionEstablishmentRequest(pdrs []*ieLib.IE, fars []*ieLib.IE, qers []*ieLib.IE) error { +func (c *PFCPClient) SendSessionEstablishmentRequest(pdrs []*ieLib.IE, fars []*ieLib.IE, + qers []*ieLib.IE, urrs []*ieLib.IE, +) error { estReq := message.NewSessionEstablishmentRequest( 0, 0, @@ -251,11 +365,12 @@ func (c *PFCPClient) SendSessionEstablishmentRequest(pdrs []*ieLib.IE, fars []*i estReq.CreatePDR = append(estReq.CreatePDR, pdrs...) estReq.CreateFAR = append(estReq.CreateFAR, fars...) estReq.CreateQER = append(estReq.CreateQER, qers...) + estReq.CreateURR = append(estReq.CreateURR, urrs...) return c.sendMsg(estReq) } -func (c *PFCPClient) SendSessionModificationRequest(PeerSEID uint64, pdrs []*ieLib.IE, qers []*ieLib.IE, fars []*ieLib.IE) error { +func (c *PFCPClient) SendSessionModificationRequest(PeerSEID uint64, pdrs []*ieLib.IE, qers []*ieLib.IE, fars []*ieLib.IE, urrs []*ieLib.IE) error { modifyReq := message.NewSessionModificationRequest( 0, 0, @@ -267,6 +382,7 @@ func (c *PFCPClient) SendSessionModificationRequest(PeerSEID uint64, pdrs []*ieL modifyReq.UpdatePDR = append(modifyReq.UpdatePDR, pdrs...) modifyReq.UpdateFAR = append(modifyReq.UpdateFAR, fars...) modifyReq.UpdateQER = append(modifyReq.UpdateQER, qers...) + modifyReq.UpdateURR = append(modifyReq.UpdateURR, urrs...) return c.sendMsg(modifyReq) } @@ -332,7 +448,7 @@ func (c *PFCPClient) SetupAssociation() error { assocResp, ok := resp.(*message.AssociationSetupResponse) if !ok { - return NewInvalidResponseError() + return NewInvalidResponseError(wrongRspType) } cause, err := assocResp.Cause.Cause() @@ -341,7 +457,7 @@ func (c *PFCPClient) SetupAssociation() error { } if cause != ieLib.CauseRequestAccepted { - return NewInvalidResponseError() + return NewInvalidResponseError(assocFailed) } ctx, cancelFunc := context.WithCancel(c.ctx) @@ -393,12 +509,14 @@ func (c *PFCPClient) TeardownAssociation() error { // EstablishSession sends PFCP Session Establishment Request and waits for PFCP Session Establishment Response. // Returns a pointer to a new PFCPSession. Returns error if the process fails at any stage. -func (c *PFCPClient) EstablishSession(pdrs []*ieLib.IE, fars []*ieLib.IE, qers []*ieLib.IE) (*PFCPSession, error) { +func (c *PFCPClient) EstablishSession(pdrs []*ieLib.IE, fars []*ieLib.IE, + qers []*ieLib.IE, urrs []*ieLib.IE, +) (*PFCPSession, error) { if !c.isAssociationActive { return nil, NewAssociationInactiveError() } - err := c.SendSessionEstablishmentRequest(pdrs, fars, qers) + err := c.SendSessionEstablishmentRequest(pdrs, fars, qers, urrs) if err != nil { return nil, err } @@ -430,12 +548,14 @@ func (c *PFCPClient) EstablishSession(pdrs []*ieLib.IE, fars []*ieLib.IE, qers [ return sess, nil } -func (c *PFCPClient) ModifySession(sess *PFCPSession, pdrs []*ieLib.IE, fars []*ieLib.IE, qers []*ieLib.IE) error { +func (c *PFCPClient) ModifySession(sess *PFCPSession, pdrs []*ieLib.IE, fars []*ieLib.IE, + qers []*ieLib.IE, urrs []*ieLib.IE, +) error { if !c.isAssociationActive { return NewAssociationInactiveError() } - err := c.SendSessionModificationRequest(sess.peerSEID, pdrs, fars, qers) + err := c.SendSessionModificationRequest(sess.peerSEID, pdrs, fars, qers, urrs) if err != nil { return err } diff --git a/pkg/pfcpsim/session/far_builder.go b/pkg/pfcpsim/session/far_builder.go index e6a1c3c..88d959f 100644 --- a/pkg/pfcpsim/session/far_builder.go +++ b/pkg/pfcpsim/session/far_builder.go @@ -4,6 +4,8 @@ package session import ( + "log" + "github.com/wmnsk/go-pfcp/ie" ) @@ -20,11 +22,36 @@ type farBuilder struct { isInterfaceSet bool } +const ( + FarNoFuzz = 0 + FarWithAction = 1 + FarWithTEID = 2 + FarWithDstInterface = 3 + FarMax = 4 +) + // NewFARBuilder returns a farBuilder. func NewFARBuilder() *farBuilder { return &farBuilder{} } +func (b *farBuilder) FuzzIE(ieType int, arg uint) *farBuilder { + switch ieType { + case FarWithAction: + log.Println("FarWithAction") + return b.WithAction(uint8(arg)) + case FarWithTEID: + log.Println("FarWithTEID") + return b.WithTEID(uint32(arg)) + case FarWithDstInterface: + log.Println("FarWithDstInterface") + return b.WithDstInterface(uint8(arg)) + default: + } + + return b +} + func (b *farBuilder) WithID(id uint32) *farBuilder { b.farID = id return b @@ -85,7 +112,9 @@ func (b *farBuilder) validate() { // BuildFAR returns a downlinkFAR if MarkAsDownlink was invoked. // Returns an UplinkFAR if MarkAsUplink was invoked. func (b *farBuilder) BuildFAR() *ie.IE { - b.validate() + if doCheck { + b.validate() + } fwdParams := ie.NewForwardingParameters( ie.NewDestinationInterface(b.dstInterface), diff --git a/pkg/pfcpsim/session/pdr_builder.go b/pkg/pfcpsim/session/pdr_builder.go index 457b443..6d5024c 100644 --- a/pkg/pfcpsim/session/pdr_builder.go +++ b/pkg/pfcpsim/session/pdr_builder.go @@ -4,6 +4,7 @@ package session import ( + "log" "net" "github.com/wmnsk/go-pfcp/ie" @@ -24,12 +25,47 @@ type pdrBuilder struct { direction direction } +var doCheck = true + +func SetCheck(check bool) { + doCheck = check +} + +const ( + PdrNoFuzz = 0 + PdrWithPrecedence = 1 + PdrWithTEID = 2 + PdrAddQERID = 3 + PdrWithFARID = 4 + PdrMax = 5 +) + func NewPDRBuilder() *pdrBuilder { return &pdrBuilder{ qerIDs: make([]*ie.IE, 0), } } +func (b *pdrBuilder) FuzzIE(ieType int, arg uint) *pdrBuilder { + switch ieType { + case PdrWithPrecedence: + log.Println("PdrWithPrecedence") + return b.WithPrecedence(uint32(arg)) + case PdrWithTEID: + log.Println("PdrWithTEID") + return b.WithTEID(uint32(arg)) + case PdrAddQERID: + log.Println("PdrAddQERID") + return b.AddQERID(uint32(arg)) + case PdrWithFARID: + log.Println("PdrWithFARID") + return b.WithFARID(uint32(arg)) + default: + } + + return b +} + func (b *pdrBuilder) WithPrecedence(precedence uint32) *pdrBuilder { b.precedence = precedence return b @@ -122,7 +158,9 @@ func newRemovePDR(pdr *ie.IE) *ie.IE { // BuildPDR returns by default an UplinkFAR. // Returns a DownlinkFAR if MarkAsDownlink was invoked. func (b *pdrBuilder) BuildPDR() *ie.IE { - b.validate() + if doCheck { + b.validate() + } createFunc := ie.NewCreatePDR if b.method == Update { diff --git a/pkg/pfcpsim/session/qer_builder.go b/pkg/pfcpsim/session/qer_builder.go index 4bce00e..ff2da9c 100644 --- a/pkg/pfcpsim/session/qer_builder.go +++ b/pkg/pfcpsim/session/qer_builder.go @@ -3,7 +3,11 @@ package session -import "github.com/wmnsk/go-pfcp/ie" +import ( + "log" + + "github.com/wmnsk/go-pfcp/ie" +) type qerBuilder struct { method IEMethod @@ -20,10 +24,47 @@ type qerBuilder struct { isIDSet bool } +const ( + QerNoFuzz = 0 + QerWithQFI = 1 + QerWithUplinkMBR = 2 + QerWithUplinkGBR = 3 + QerWithDownlinkMBR = 4 + QerWithDownlinkGBR = 5 + QerWithGateStatus = 6 + QerMax = 7 +) + func NewQERBuilder() *qerBuilder { return &qerBuilder{} } +func (b *qerBuilder) FuzzIE(ieType int, arg uint) *qerBuilder { + switch ieType { + case QerWithQFI: + log.Println("QerWithQFI") + return b.WithQFI(uint8(arg)) + case QerWithUplinkMBR: + log.Println("QerWithUplinkMBR") + return b.WithUplinkMBR(uint64(arg)) + case QerWithUplinkGBR: + log.Println("QerWithUplinkGBR") + return b.WithUplinkGBR(uint64(arg)) + case QerWithDownlinkMBR: + log.Println("QerWithDownlinkMBR") + return b.WithDownlinkMBR(uint64(arg)) + case QerWithDownlinkGBR: + log.Println("QerWithDownlinkGBR") + return b.WithDownlinkGBR(uint64(arg)) + case QerWithGateStatus: + log.Println("QerWithGateStatus") + return b.WithGateStatus(uint8(arg)) + default: + } + + return b +} + func (b *qerBuilder) WithID(id uint32) *qerBuilder { // Used to avoid using 0 as default value. It makes sure that WithID was invoked. b.isIDSet = true @@ -83,7 +124,9 @@ func (b *qerBuilder) WithMethod(method IEMethod) *qerBuilder { } func (b *qerBuilder) Build() *ie.IE { - b.validate() + if doCheck { + b.validate() + } createFunc := ie.NewCreateQER if b.method == Update { diff --git a/pkg/pfcpsim/session/urr_build_test.go b/pkg/pfcpsim/session/urr_build_test.go new file mode 100644 index 0000000..7711adc --- /dev/null +++ b/pkg/pfcpsim/session/urr_build_test.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024-present Ian Chen + +package session + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/wmnsk/go-pfcp/ie" +) + +func TestURRBuilderShouldPanic(t *testing.T) { + type testCase struct { + input *urrBuilder + expected *urrBuilder + description string + } + + for _, scenario := range []testCase{ + { + input: NewURRBuilder(). + WithID(1). + WithMethod(Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(0). + WithMeasurementInfo(17). + WithReportingTrigger(ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }), + expected: &urrBuilder{ + method: Create, + urrID: 1, + measurementInfo: 17, + measurementMethod: &measurementMethodParams{ + event: 0, + volum: 1, + durat: 0, + }, + measurementPeriod: 0, + rptTrig: ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }, + }, + description: "Invalid URR: measurementInfo is invalid", + }, + } { + t.Run(scenario.description, func(t *testing.T) { + assert.Panics(t, func() { scenario.input.Build() }) + assert.Equal(t, scenario.input, scenario.expected) + }) + } +} + +func TestURRBuilder(t *testing.T) { + type testCase struct { + input *urrBuilder + expected *ie.IE + description string + } + + for _, scenario := range []testCase{ + { + input: NewURRBuilder(). + WithID(1). + WithMethod(Create). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(time.Second). + WithReportingTrigger(ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }), + expected: ie.NewCreateURR( + ie.NewURRID(1), + ie.NewMeasurementMethod(0, 1, 0), + ie.NewMeasurementPeriod(time.Second), + NewRptTrig(ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }), + ), + description: "Valid Create URR with reporting trigger", + }, + { + input: NewURRBuilder(). + WithID(1). + WithMethod(Update). + WithMeasurementPeriod(2 * time.Second), + expected: ie.NewUpdateURR( + ie.NewURRID(1), + ie.NewMeasurementPeriod(2*time.Second), + ), + description: "Valid Update URR", + }, + { + input: NewURRBuilder(). + WithID(1). + WithMethod(Delete). + WithMeasurementMethod(0, 1, 0). + WithMeasurementPeriod(1). + WithReportingTrigger(ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }), + expected: ie.NewRemoveURR( + ie.NewCreateURR( + ie.NewURRID(1), + ie.NewMeasurementMethod(0, 1, 0), + ie.NewMeasurementPeriod(2), + NewRptTrig(ReportingTrigger{ + Flags: RPT_TRIG_PERIO, + }), + ), + ), + description: "Valid Delete URR", + }, + } { + t.Run(scenario.description, func(t *testing.T) { + assert.NotPanics(t, func() { _ = scenario.input.Build() }) + assert.Equal(t, scenario.expected, scenario.input.Build()) + }) + } +} diff --git a/pkg/pfcpsim/session/urr_builder.go b/pkg/pfcpsim/session/urr_builder.go new file mode 100644 index 0000000..9d90238 --- /dev/null +++ b/pkg/pfcpsim/session/urr_builder.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024-present Ian Chen + +package session + +import ( + "log" + "time" + + "github.com/wmnsk/go-pfcp/ie" +) + +type measurementMethodParams struct { + event int + volum int + durat int +} + +type urrBuilder struct { + urrID uint32 + method IEMethod + measurementMethod *measurementMethodParams + measurementPeriod time.Duration + measurementInfo uint8 + rptTrig ReportingTrigger + volumThreshold *volumThreshold + volumeQuota *volumeQuota +} + +const ( + UrrNoFuzz = 0 + UrrWithMeasurementInfo = 1 + UrrWithMeasurementMethod = 2 + UrrWithMeasurementPeriod = 3 + UrrMax = 4 +) + +// NewURRBuilder returns a urrBuilder. +func NewURRBuilder() *urrBuilder { + return &urrBuilder{} +} + +func (b *urrBuilder) FuzzIE(ieType int, arg uint) *urrBuilder { + switch ieType { + case UrrWithMeasurementInfo: + log.Println("Fuzz: UrrWithMeasurementInfo") + return b.WithMeasurementInfo(uint8(arg)) + case UrrWithMeasurementMethod: + log.Println("Fuzz: UrrWithMeasurementMethod") + + args := []int{0, 1, 0} + i := arg % 3 + args[i] = int(arg) + + return b.WithMeasurementMethod(args[0], args[1], args[2]) + case UrrWithMeasurementPeriod: + log.Println("Fuzz: UrrWithMeasurementPeriod") + return b.WithMeasurementPeriod(time.Duration(arg)) + default: + } + + return b +} + +func (b *urrBuilder) WithID(id uint32) *urrBuilder { + b.urrID = id + return b +} + +func (b *urrBuilder) WithMethod(method IEMethod) *urrBuilder { + b.method = method + return b +} + +func (b *urrBuilder) WithMeasurementMethod(event, volum, durat int) *urrBuilder { + b.measurementMethod = &measurementMethodParams{ + event: event, + volum: volum, + durat: durat, + } + + return b +} + +func (b *urrBuilder) WithMeasurementPeriod(period time.Duration) *urrBuilder { + b.measurementPeriod = period + return b +} + +func (b *urrBuilder) WithMeasurementInfo(info uint8) *urrBuilder { + b.measurementInfo = info + return b +} + +func (b *urrBuilder) WithVolumeThreshold(flags uint8, tvol, uvol, dvol uint64) *urrBuilder { + b.volumThreshold = &volumThreshold{ + flags: flags, + tvol: tvol, + uvol: uvol, + dvol: dvol, + } + + return b +} + +func (b *urrBuilder) WithVolumeQuota(flags uint8, tvol, uvol, dvol uint64) *urrBuilder { + b.volumeQuota = &volumeQuota{ + flags: flags, + tvol: tvol, + uvol: uvol, + dvol: dvol, + } + + return b +} + +// TS 29.244 5.2.2.2 +// When provisioning a URR, the CP function shall provide the reporting trigger(s) in the Reporting Triggers IE of the +// URR which shall cause the UP function to generate and send a Usage Report for this URR to the CP function. +func (b *urrBuilder) WithReportingTrigger(rptTrig ReportingTrigger) *urrBuilder { + b.rptTrig = rptTrig + return b +} + +func (b *urrBuilder) validate() { + if b.urrID == 0 { + panic("URR ID is not set") + } + + if b.measurementInfo > 0 && b.measurementInfo > MNOP { + panic("Measurement Information is not valid") + } +} + +func (b *urrBuilder) Build() *ie.IE { + if doCheck { + b.validate() + } + + createFunc := ie.NewCreateURR + if b.method == Update { + createFunc = ie.NewUpdateURR + } + + urr := createFunc(ie.NewURRID(b.urrID), + newMeasurementMethod(b.measurementMethod), + ie.NewMeasurementPeriod(b.measurementPeriod), + NewRptTrig(b.rptTrig), + newVolumeThreshold(b.volumThreshold), + newVolumeQuota(b.volumeQuota), + newMeasurementInfo(b.measurementInfo), + ) + + if b.method == Delete { + return ie.NewRemoveURR(urr) + } + + return urr +} diff --git a/pkg/pfcpsim/session/urr_ies.go b/pkg/pfcpsim/session/urr_ies.go new file mode 100644 index 0000000..36d0c96 --- /dev/null +++ b/pkg/pfcpsim/session/urr_ies.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024-present Ian Chen +package session + +import ( + "encoding/binary" + + "github.com/wmnsk/go-pfcp/ie" +) + +// Measurement Information IE bits definition +const ( + MBQE = 1 << iota // Measurement Before QoS Enforcement + INAM // Inactive Measurement + RADI // Reduced Application Detection Information + ISTM // Immediate Start Time Metering + MNOP // Measurement of Number of Packets +) + +func newMeasurementInfo(info uint8) *ie.IE { + if info == 0 { + return nil + } + + return ie.NewMeasurementInformation(info) +} + +// Reporting Triggers IE bits definition +const ( + RPT_TRIG_PERIO = 1 << iota + RPT_TRIG_VOLTH + RPT_TRIG_TIMTH + RPT_TRIG_QUHTI + RPT_TRIG_START + RPT_TRIG_STOPT + RPT_TRIG_DROTH + RPT_TRIG_LIUSA + RPT_TRIG_VOLQU + RPT_TRIG_TIMQU + RPT_TRIG_ENVCL + RPT_TRIG_MACAR + RPT_TRIG_EVETH + RPT_TRIG_EVEQU + RPT_TRIG_IPMJL + RPT_TRIG_QUVTI + RPT_TRIG_REEMR + RPT_TRIG_UPINT +) + +type ReportingTrigger struct { + Flags uint32 +} + +func NewRptTrig(rpgTrig ReportingTrigger) *ie.IE { + if rpgTrig.Flags == 0 { + return nil + } + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, rpgTrig.Flags) + + if b[2] != 0 { + return ie.NewReportingTriggers(b[:3]...) + } + + return ie.NewReportingTriggers(b[:2]...) +} + +// Volume Threshold IE +type volumThreshold struct { + flags uint8 + tvol uint64 + uvol uint64 + dvol uint64 +} + +func newVolumeThreshold(vParams *volumThreshold) *ie.IE { + if vParams == nil { + return nil + } + + return ie.NewVolumeThreshold(vParams.flags, vParams.tvol, vParams.uvol, vParams.dvol) +} + +// Volume Measurement IE Flag bits definition +const ( + TOVOL uint8 = 1 << iota + ULVOL + DLVOL + TONOP + ULNOP + DLNOP +) + +// Volume Threshold IE +type volumeQuota struct { + flags uint8 + tvol uint64 + uvol uint64 + dvol uint64 +} + +func newVolumeQuota(vParams *volumeQuota) *ie.IE { + if vParams == nil { + return nil + } + + return ie.NewVolumeQuota(vParams.flags, vParams.tvol, vParams.uvol, vParams.dvol) +} + +func newMeasurementMethod(mParams *measurementMethodParams) *ie.IE { + if mParams == nil { + return nil + } + + return ie.NewMeasurementMethod(mParams.event, mParams.volum, mParams.durat) +}