├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── main.yml │ ├── push.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .spellcheck.yml ├── .wordlist.txt ├── Dockerfile ├── LICENSES ├── Apache-2.0.txt └── BSD-3-Clause.txt ├── Makefile ├── README.md ├── VERSION ├── VERSION.license ├── cmd ├── p4info_code_gen │ ├── p4info_code_gen.go │ └── p4info_code_gen_test.go └── pfcpiface │ └── main.go ├── conf ├── __init__.py ├── closed_loop.bess ├── closed_loop_svr.py ├── cndp_upf_1worker.jsonc ├── cndp_upf_4worker.jsonc ├── cndp_upf_8worker.jsonc ├── grafana │ ├── dashboards │ │ └── upf-custom.yml │ ├── datasources │ │ └── prometheus.yml │ ├── gtpu-path-monitoring.json │ ├── gtpu-path-monitoring.json.license │ ├── upf-custom.json │ ├── upf-custom.json.license │ ├── upf-session-summary-latency-heatmap.json │ ├── upf-session-summary-latency-heatmap.json.license │ ├── upf-session-summary.json │ ├── upf-session-summary.json.license │ ├── upf-session.json │ ├── upf-session.json.license │ └── upf-session.license ├── gtp_echo.py ├── p4 │ └── bin │ │ ├── bmv2.json │ │ ├── bmv2.json.license │ │ ├── p4info.txt │ │ └── p4info.txt.license ├── parser.py ├── pktgen.bess ├── pktgen_cndp.bess ├── ports.py ├── prometheus.yml ├── route_control.py ├── sim.py ├── test_route_control.py ├── up4.bess ├── upf.jsonc └── utils.py ├── deployments └── upf-k8s.yaml ├── docs ├── CNDP_README.md ├── INSTALL.md ├── configuration-guide.md ├── developer-guide.md ├── dpdk-configuration.md ├── images │ ├── bess-programming.svg │ ├── bess-programming.svg.license │ ├── bess-upf-on-ec2.svg │ ├── bess-upf-on-ec2.svg.license │ ├── cndp-omec-upf-test-setup.jpg │ ├── cndp-omec-upf-test-setup.jpg.license │ ├── pipeline.svg │ ├── pipeline.svg.license │ ├── ubench-pktgen.svg │ ├── ubench-pktgen.svg.license │ ├── ubench-sim.svg │ ├── ubench-sim.svg.license │ ├── upf-overview.jpg │ ├── upf-overview.jpg.license │ ├── upf.svg │ └── upf.svg.license ├── pipeline.txt ├── pipeline.txt.license └── running-upf-on-ec2.md ├── go.mod ├── go.mod.license ├── go.sum ├── go.sum.license ├── internal └── p4constants │ └── p4constants.go ├── logger └── logger.go ├── pfcpiface ├── bess.go ├── bess_pb │ ├── bess_msg.pb.go │ ├── error.pb.go │ ├── module_msg.pb.go │ ├── ports │ │ └── port_msg.pb.go │ ├── service.pb.go │ └── util_msg.pb.go ├── config.go ├── config_test.go ├── conn.go ├── datapath.go ├── errors.go ├── fteid.go ├── fteid_test.go ├── grpcsim.go ├── ip_pool.go ├── ip_pool_test.go ├── local_store.go ├── messages.go ├── messages_conn.go ├── messages_session.go ├── metrics.txt ├── metrics.txt.license ├── metrics │ ├── interface.go │ ├── prometheus.go │ └── prometheus_test.go ├── node.go ├── notifier.go ├── notifier_test.go ├── p4rt_translator.go ├── p4rtc.go ├── parse_far.go ├── parse_far_test.go ├── parse_pdr.go ├── parse_pdr_test.go ├── parse_qer.go ├── parse_qer_test.go ├── parse_sdf.go ├── parse_sdf_test.go ├── pfcpiface.go ├── pfd.go ├── session_far.go ├── session_pdr.go ├── session_qer.go ├── sessions.go ├── sessions_store.go ├── telemetry.go ├── telemetry_test.go ├── up4.go ├── upf.go ├── utils.go ├── utils_test.go └── web_service.go ├── pkg ├── fake_bess │ ├── fake_bess.go │ └── fake_bess_service.go └── utils │ ├── utils.go │ └── utils_test.go ├── ptf ├── .env ├── Dockerfile ├── Makefile ├── README.md ├── config │ ├── docker_setup.sh │ ├── trex-cfg-for-ptf.yaml │ └── upf.jsonc ├── docs │ ├── test-run.svg │ ├── test-run.svg.license │ ├── upf-access.svg │ └── upf-access.svg.license ├── jenkins.sh ├── lib │ ├── .gitignore │ ├── __init__.py │ ├── grpc_test.py │ ├── pkt_utils.py │ ├── ptf_runner.py │ ├── trex_test.py │ └── trex_utils.py ├── run_tests └── tests │ ├── linerate │ ├── baseline.py │ ├── common.py │ ├── mbr.py │ └── qos_metrics.py │ └── unary │ └── check_rules.py ├── requirements.txt ├── scripts ├── docker_setup.sh ├── install_ntf.sh ├── reset_upf.sh └── telemetry.sh └── test └── integration ├── README.md ├── basic_test.go ├── conf.go ├── framework.go ├── providers ├── docker.go └── p4runtime.go ├── verify_bess.go └── verify_up4.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2019-present Intel Corporation 3 | output 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2019 Intel Corporation 3 | * text eol=lf 4 | *.bess text diff=python 5 | *.jpg binary 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | 5 | --- 6 | name: Bug report 7 | about: Create a report to help us improve 8 | title: '' 9 | labels: bug 10 | assignees: '' 11 | 12 | --- 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Logs** 24 | Logs with the actual error and context 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | 5 | --- 6 | name: Feature request 7 | about: Suggest an idea for this project 8 | title: '' 9 | labels: enhancement 10 | assignees: '' 11 | 12 | --- 13 | 14 | **Is your feature request related to a problem? Please describe.** 15 | A clear and concise description of what the problem is. 16 | 17 | **Describe the solution you'd like** 18 | A clear and concise description of what you want to happen. 19 | 20 | **Describe alternatives you've considered** 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | **Additional context** 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020-Present Intel Corporation 3 | # Copyright 2025 Canonical Ltd. 4 | 5 | version: 2 6 | updates: 7 | 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | day: "sunday" 13 | time: "21:00" 14 | timezone: "America/Los_Angeles" 15 | 16 | - package-ecosystem: "docker" 17 | directory: "/ptf" 18 | schedule: 19 | interval: "weekly" 20 | day: "sunday" 21 | time: "21:00" 22 | timezone: "America/Los_Angeles" 23 | 24 | - package-ecosystem: "gomod" 25 | directory: "/pfcpiface" 26 | schedule: 27 | interval: "weekly" 28 | day: "sunday" 29 | time: "21:00" 30 | timezone: "America/Los_Angeles" 31 | 32 | - package-ecosystem: "pip" 33 | directory: "/" 34 | schedule: 35 | interval: "weekly" 36 | day: "sunday" 37 | time: "21:00" 38 | timezone: "America/Los_Angeles" 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2023 Canonical Ltd. 3 | # Copyright 2024 Intel Corporation 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | if: github.event_name == 'pull_request' 15 | uses: omec-project/.github/.github/workflows/upf-build-upf.yml@main 16 | with: 17 | branch_name: ${{ github.ref }} 18 | 19 | build-ptf: 20 | if: github.event_name == 'pull_request' 21 | uses: omec-project/.github/.github/workflows/upf-build-ptf.yml@main 22 | with: 23 | branch_name: ${{ github.ref }} 24 | 25 | lint: 26 | uses: omec-project/.github/.github/workflows/lint.yml@main 27 | with: 28 | branch_name: ${{ github.ref }} 29 | 30 | hadolint: 31 | uses: omec-project/.github/.github/workflows/hadolint.yml@main 32 | with: 33 | branch_name: ${{ github.ref }} 34 | 35 | check-spelling: 36 | uses: omec-project/.github/.github/workflows/check-spelling.yml@main 37 | with: 38 | branch_name: ${{ github.ref }} 39 | 40 | route-control-tests: 41 | uses: omec-project/.github/.github/workflows/upf-route-control-tests.yml@main 42 | with: 43 | branch_name: ${{ github.ref }} 44 | 45 | unit-tests-pfcp: 46 | uses: omec-project/.github/.github/workflows/unit-test.yml@main 47 | with: 48 | branch_name: ${{ github.ref }} 49 | test_flags: "-race -failfast -v" 50 | test_directory: "./pfcpiface ./cmd/..." 51 | 52 | integration-tests-up4: 53 | uses: omec-project/.github/.github/workflows/unit-test.yml@main 54 | with: 55 | branch_name: ${{ github.ref }} 56 | test_flags: "-v -count=1 -failfast -timeout 15m" 57 | test_directory: "./test/integration/..." 58 | env_vars: "DOCKER_TARGETS=pfcpiface DOCKER_TAG=integration make docker-build && MODE=docker DATAPATH=up4" 59 | 60 | integration-tests-bess: 61 | uses: omec-project/.github/.github/workflows/unit-test.yml@main 62 | with: 63 | branch_name: ${{ github.ref }} 64 | test_flags: "-race -failfast -v -count=1" 65 | test_directory: "./test/integration/..." 66 | env_vars: "MODE=native DATAPATH=bess" 67 | 68 | license-check: 69 | uses: omec-project/.github/.github/workflows/license-check.yml@main 70 | with: 71 | branch_name: ${{ github.ref }} 72 | 73 | fossa-scan: 74 | uses: omec-project/.github/.github/workflows/fossa-scan.yml@main 75 | with: 76 | branch_name: ${{ github.ref }} 77 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2024 Intel Corporation 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "VERSION" 9 | 10 | jobs: 11 | tag-github: 12 | uses: omec-project/.github/.github/workflows/tag-github.yml@main 13 | secrets: inherit 14 | 15 | release-image: 16 | needs: tag-github 17 | uses: omec-project/.github/.github/workflows/upf-release-image.yml@main 18 | with: 19 | branch_name: ${{ github.ref }} 20 | changed: ${{ needs.tag-github.outputs.changed }} 21 | version: ${{ needs.tag-github.outputs.version }} 22 | secrets: inherit 23 | 24 | update-version: 25 | needs: tag-github 26 | uses: omec-project/.github/.github/workflows/update-version.yml@main 27 | with: 28 | changed: ${{ needs.tag-github.outputs.changed }} 29 | version: ${{ needs.tag-github.outputs.version }} 30 | secrets: inherit 31 | 32 | branch-release: 33 | needs: tag-github 34 | uses: omec-project/.github/.github/workflows/branch-release.yml@main 35 | with: 36 | release_branch: ${{ needs.tag-github.outputs.release_branch }} 37 | version_branch: ${{ needs.tag-github.outputs.version_branch }} 38 | secrets: inherit 39 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2019-present Intel Corporation 3 | # Copyright 2025 Canonical Ltd. 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | stale: 10 | uses: omec-project/.github/.github/workflows/stale-issue.yml@main 11 | with: 12 | days_before_stale: 120 13 | days_before_close: 15 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2019 Intel Corporation 3 | # Copyright 2023 Canonical Ltd. 4 | 5 | .idea/ 6 | .coverage/ 7 | *.pyc 8 | *.swp 9 | *.log 10 | output 11 | dpdk-devbind.py 12 | dpdk-hugepages.py 13 | dictionary.dic 14 | venv/ 15 | ptf/log/ 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | version: "2" 5 | run: 6 | tests: true 7 | linters: 8 | enable: 9 | - asciicheck 10 | - dogsled 11 | - goconst 12 | - gomodguard 13 | - govet 14 | - misspell 15 | - noctx 16 | - nilerr 17 | - nilnil 18 | - predeclared 19 | - unconvert 20 | - unparam 21 | - unused 22 | - whitespace 23 | disable: 24 | - errcheck 25 | - godox 26 | - nakedret 27 | - staticcheck 28 | settings: 29 | errcheck: 30 | check-type-assertions: false 31 | check-blank: true 32 | goconst: 33 | min-len: 3 34 | min-occurrences: 3 35 | govet: 36 | enable-all: true 37 | disable: 38 | - fieldalignment 39 | settings: 40 | printf: 41 | funcs: 42 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 43 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 44 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 45 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 46 | exclusions: 47 | generated: lax 48 | presets: 49 | - comments 50 | - common-false-positives 51 | - legacy 52 | - std-error-handling 53 | paths: 54 | - .*\.pb\.go 55 | - third_party$ 56 | - builtin$ 57 | - examples$ 58 | issues: 59 | uniq-by-line: true 60 | formatters: 61 | enable: 62 | - gofmt 63 | - goimports 64 | exclusions: 65 | generated: lax 66 | paths: 67 | - .*\.pb\.go 68 | - third_party$ 69 | - builtin$ 70 | - examples$ 71 | -------------------------------------------------------------------------------- /.spellcheck.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2023 Intel Corporation 3 | 4 | matrix: 5 | - name: Markdown 6 | # expect_match: false 7 | apsell: 8 | lang: en 9 | # ignore-case: true 10 | dictionary: 11 | wordlists: 12 | - .wordlist.txt 13 | # output: wordlist.dic 14 | encoding: utf-8 15 | pipeline: 16 | - pyspelling.filters.markdown: 17 | markdown_extensions: 18 | - markdown.extensions.extra: 19 | - pyspelling.filters.html: 20 | comments: false 21 | attributes: 22 | - title 23 | - alt 24 | ignores: 25 | - :matches(code, pre) 26 | - code 27 | - pre 28 | - blockquote 29 | sources: 30 | - '**/*.md' 31 | default_encoding: utf-8 32 | -------------------------------------------------------------------------------- /.wordlist.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2023 Intel Corporation 3 | adaptor 4 | ADQ 5 | aether 6 | Aether 7 | allocatable 8 | AMI 9 | bb 10 | bess 11 | busypoll 12 | BUIDKIT 13 | cndp 14 | CNDP 15 | CNI 16 | config 17 | CPUs 18 | datagrams 19 | datapath 20 | Datapath 21 | datapaths 22 | DDN 23 | DDNs 24 | DDP 25 | dev 26 | devbind 27 | devlink 28 | dmac 29 | Dockerfile 30 | DockerHub 31 | downlink 32 | Downlink 33 | dpdk 34 | DPDK 35 | DSCP 36 | dst 37 | ec 38 | eNB 39 | ENA 40 | ENI 41 | ENIs 42 | eng 43 | enp 44 | epc 45 | ethernet 46 | FARs 47 | github 48 | gNB 49 | gRPC 50 | GTP 51 | GTPu 52 | html 53 | http 54 | https 55 | hugepages 56 | HugePages 57 | HW 58 | HVM 59 | ifaces 60 | ifname 61 | IOMMU 62 | IOV 63 | IPs 64 | ip 65 | irq 66 | irqbalance 67 | jpg 68 | json 69 | jsonc 70 | kubernetes 71 | linerate 72 | Linerate 73 | linux 74 | localhost 75 | lports 76 | MAKEFLAGS 77 | md 78 | Microbenchmarks 79 | Natting 80 | natting 81 | netdev 82 | NIC 83 | NICs 84 | NodeID 85 | NUMA 86 | nproc 87 | NTF 88 | omec 89 | ONF 90 | ONF's 91 | ONFConnect 92 | ONOS 93 | pci 94 | PCI 95 | PCIe 96 | PDR 97 | PDRs 98 | pfcpiface 99 | PFCP 100 | pfcpsim 101 | PFDs 102 | pktgen 103 | protobuf 104 | PTF 105 | QERs 106 | qid 107 | QoS 108 | README 109 | RSS 110 | runtime 111 | Scapy 112 | SDF 113 | setenv 114 | SMF 115 | SPGW 116 | smac 117 | sourceforge 118 | SR-IOV 119 | subnet 120 | subnet's 121 | subnets 122 | sudo 123 | tc 124 | TCP 125 | tcpdump 126 | TEID 127 | TRex 128 | UDP 129 | UE 130 | UEs 131 | ULCL 132 | unary 133 | Unary 134 | uncomment 135 | unencapsulated 136 | upf 137 | UPF 138 | UPF's 139 | vendoring 140 | vfio 141 | VPC 142 | VM 143 | www 144 | WIP 145 | YAML 146 | # Words with numbers or symbols 147 | af 148 | BMv 149 | Gi 150 | GPP 151 | IPv 152 | xdp 153 | XDP 154 | xlarge 155 | # Words part of hyperlinks 156 | conf 157 | hostip 158 | io 159 | networktokens 160 | ubench 161 | # Words inside code blocks 162 | BESS's 163 | cidr 164 | CMDLINE 165 | DPDK's 166 | elif 167 | hugepage 168 | hugepagesz 169 | intel 170 | iommu 171 | ipaddrs 172 | IPC 173 | macaddrs 174 | netns 175 | nhipaddrs 176 | nhmacaddrs 177 | Personalization 178 | PRIVS 179 | rf 180 | sgi 181 | vdev 182 | zc 183 | ZC 184 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020-present Open Networking Foundation 3 | # Copyright 2019-present Intel Corporation 4 | 5 | # Stage bess-build: fetch BESS dependencies & pre-reqs 6 | FROM registry.aetherproject.org/sdcore/bess_build:241202 AS bess-build 7 | ARG CPU=native 8 | ARG BESS_COMMIT=main 9 | ENV PLUGINS_DIR=plugins 10 | ARG MAKEFLAGS 11 | ENV PKG_CONFIG_PATH=/usr/lib64/pkgconfig 12 | 13 | RUN apt-get update && apt-get install -y \ 14 | --no-install-recommends \ 15 | git \ 16 | ca-certificates \ 17 | libbpf0 \ 18 | libelf-dev && \ 19 | apt-get clean && \ 20 | rm -rf /var/lib/apt/lists/* 21 | 22 | # BESS pre-reqs 23 | WORKDIR /bess 24 | RUN git clone https://github.com/omec-project/bess.git . && \ 25 | git checkout ${BESS_COMMIT} && \ 26 | cp -a protobuf /protobuf 27 | 28 | # Build DPDK 29 | RUN ./build.py dpdk 30 | 31 | # Plugins: SequentialUpdate 32 | RUN mkdir -p plugins && \ 33 | mv sample_plugin plugins 34 | 35 | ## Network Token 36 | ARG ENABLE_NTF 37 | ARG NTF_COMMIT=master 38 | COPY scripts/install_ntf.sh . 39 | RUN ./install_ntf.sh 40 | 41 | # Build and copy artifacts 42 | RUN PLUGINS=$(find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 -type d) && \ 43 | CMD="./build.py bess" && \ 44 | for PLUGIN in $PLUGINS; do \ 45 | CMD="$CMD --plugin \"$PLUGIN\""; \ 46 | done && \ 47 | eval "$CMD" && \ 48 | cp bin/bessd /bin && \ 49 | mkdir -p /bin/modules && \ 50 | cp core/modules/*.so /bin/modules && \ 51 | mkdir -p /opt/bess && \ 52 | cp -r bessctl pybess /opt/bess && \ 53 | cp -r core/pb /pb 54 | 55 | # Stage bess: creates the runtime image of BESS 56 | FROM ubuntu:24.04 AS bess 57 | WORKDIR / 58 | COPY requirements.txt . 59 | RUN apt-get update && apt-get install -y \ 60 | --no-install-recommends \ 61 | python3-pip \ 62 | libgraph-easy-perl \ 63 | iproute2 \ 64 | iptables \ 65 | iputils-ping \ 66 | tcpdump && \ 67 | apt-get clean && \ 68 | rm -rf /var/lib/apt/lists/* && \ 69 | pip install --no-cache-dir --break-system-packages -r requirements.txt 70 | COPY --from=bess-build /opt/bess /opt/bess 71 | COPY --from=bess-build /bin/bessd /bin/bessd 72 | COPY --from=bess-build /bin/modules /bin/modules 73 | COPY conf /opt/bess/bessctl/conf 74 | RUN ln -s /opt/bess/bessctl/bessctl /bin 75 | 76 | # CNDP: Install dependencies 77 | ENV DEBIAN_FRONTEND=noninteractive 78 | RUN apt-get update && apt-get install -y \ 79 | --no-install-recommends \ 80 | build-essential \ 81 | ethtool \ 82 | libbsd0 \ 83 | libelf1 \ 84 | libgflags2.2 \ 85 | libjson-c[45] \ 86 | libnl-3-200 \ 87 | libnl-cli-3-200 \ 88 | libnuma1 \ 89 | libpcap0.8 \ 90 | pkg-config && \ 91 | apt-get clean && \ 92 | rm -rf /var/lib/apt/lists/* 93 | COPY --from=bess-build /usr/bin/cndpfwd /usr/bin/ 94 | COPY --from=bess-build /usr/local/lib/x86_64-linux-gnu/*.so /usr/local/lib/x86_64-linux-gnu/ 95 | COPY --from=bess-build /usr/local/lib/x86_64-linux-gnu/*.a /usr/local/lib/x86_64-linux-gnu/ 96 | COPY --from=bess-build /usr/lib/libxdp* /usr/lib/ 97 | COPY --from=bess-build /usr/lib/x86_64-linux-gnu/libjson-c.so* /lib/x86_64-linux-gnu/ 98 | COPY --from=bess-build /usr/lib/x86_64-linux-gnu/libbpf.so* /usr/lib/x86_64-linux-gnu/ 99 | 100 | ENV PYTHONPATH="/opt/bess" 101 | WORKDIR /opt/bess/bessctl 102 | ENTRYPOINT ["bessd", "-f"] 103 | 104 | # Stage build bess golang pb 105 | FROM golang:1.24.3-bookworm AS protoc-gen 106 | RUN go install github.com/golang/protobuf/protoc-gen-go@latest 107 | 108 | FROM bess-build AS go-pb 109 | COPY --from=protoc-gen /go/bin/protoc-gen-go /bin 110 | RUN mkdir /bess_pb && \ 111 | protoc -I /usr/include -I /protobuf/ \ 112 | /protobuf/*.proto /protobuf/ports/*.proto \ 113 | --go_opt=paths=source_relative --go_out=plugins=grpc:/bess_pb 114 | 115 | FROM bess-build AS py-pb 116 | RUN pip install --no-cache-dir grpcio-tools==1.26 117 | RUN mkdir /bess_pb && \ 118 | python3 -m grpc_tools.protoc -I /usr/include -I /protobuf/ \ 119 | /protobuf/*.proto /protobuf/ports/*.proto \ 120 | --python_out=plugins=grpc:/bess_pb \ 121 | --grpc_python_out=/bess_pb 122 | 123 | FROM golang:1.24.3-bookworm AS pfcpiface-build 124 | ARG GOFLAGS 125 | WORKDIR /pfcpiface 126 | 127 | COPY go.mod /pfcpiface/go.mod 128 | COPY go.sum /pfcpiface/go.sum 129 | 130 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 131 | RUN if echo "$GOFLAGS" | grep -Eq "-mod=vendor"; then go mod download; fi 132 | 133 | COPY . /pfcpiface 134 | RUN CGO_ENABLED=0 go build $GOFLAGS -o /bin/pfcpiface ./cmd/pfcpiface 135 | 136 | # Stage pfcpiface: runtime image of pfcpiface toward SMF/SPGW-C 137 | FROM alpine:3.21 AS pfcpiface 138 | COPY conf /opt/bess/bessctl/conf 139 | COPY --from=pfcpiface-build /bin/pfcpiface /bin 140 | ENTRYPOINT [ "/bin/pfcpiface" ] 141 | 142 | # Stage pb: dummy stage for collecting protobufs 143 | FROM scratch AS pb 144 | COPY --from=bess-build /bess/protobuf /protobuf 145 | COPY --from=go-pb /bess_pb /bess_pb 146 | 147 | # Stage ptf-pb: dummy stage for collecting python protobufs 148 | FROM scratch AS ptf-pb 149 | COPY --from=bess-build /bess/protobuf /protobuf 150 | COPY --from=py-pb /bess_pb /bess_pb 151 | 152 | # Stage binaries: dummy stage for collecting artifacts 153 | FROM scratch AS artifacts 154 | COPY --from=bess /bin/bessd / 155 | COPY --from=pfcpiface /bin/pfcpiface / 156 | COPY --from=bess-build /bess /bess 157 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are met: 3 | 4 | * Redistributions of source code must retain the above copyright notice, this 5 | list of conditions and the following disclaimer. 6 | 7 | * Redistributions in binary form must reproduce the above copyright notice, 8 | this list of conditions and the following disclaimer in the documentation 9 | and/or other materials provided with the distribution. 10 | 11 | * Neither the names of the copyright holders nor the names of their 12 | contributors may be used to endorse or promote products derived from this 13 | software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020-present Open Networking Foundation 3 | 4 | PROJECT_NAME := upf-epc 5 | VERSION ?= $(shell cat ./VERSION) 6 | OSTYPE := $(shell uname -s) 7 | ifeq ($(OSTYPE),Linux) 8 | NPROCS := $(shell nproc) 9 | else ifeq ($(OSTYPE),Darwin) # Assume Mac OS X 10 | NPROCS := $(shell sysctl -n hw.physicalcpu) 11 | else 12 | NPROCS := 1 13 | endif 14 | 15 | # Note that we set the target platform of Docker images to native 16 | # For a more portable image set CPU=haswell 17 | CPU ?= native 18 | 19 | # Enable Network Token Function support (see https://networktokens.org for more 20 | # information) 21 | ENABLE_NTF ?= 0 22 | 23 | ## Docker related 24 | DOCKER_REGISTRY ?= 25 | DOCKER_REPOSITORY ?= 26 | DOCKER_TAG ?= ${VERSION} 27 | DOCKER_IMAGENAME := ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}${PROJECT_NAME}:${DOCKER_TAG} 28 | DOCKER_BUILDKIT ?= 1 29 | DOCKER_BUILD_ARGS ?= --build-arg MAKEFLAGS=-j${NPROCS} --build-arg CPU 30 | DOCKER_BUILD_ARGS += --build-arg ENABLE_NTF=$(ENABLE_NTF) 31 | DOCKER_PULL ?= --pull 32 | 33 | ## Docker labels. Only set ref and commit date if committed 34 | DOCKER_LABEL_VCS_URL ?= $(shell git remote get-url $(shell git remote)) 35 | DOCKER_LABEL_VCS_REF ?= $(shell git diff-index --quiet HEAD -- && git rev-parse HEAD || echo "unknown") 36 | DOCKER_LABEL_COMMIT_DATE ?= $(shell git diff-index --quiet HEAD -- && git show -s --format=%cd --date=iso-strict HEAD || echo "unknown" ) 37 | DOCKER_LABEL_BUILD_DATE ?= $(shell date -u "+%Y-%m-%dT%H:%M:%SZ") 38 | 39 | DOCKER_TARGETS ?= bess pfcpiface 40 | 41 | # Golang grpc/protobuf generation 42 | BESS_PB_DIR ?= pfcpiface 43 | PTF_PB_DIR ?= ptf/lib 44 | 45 | # https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target 46 | docker-build: 47 | for target in $(DOCKER_TARGETS); do \ 48 | DOCKER_CACHE_ARG=""; \ 49 | if [ $(DOCKER_BUILDKIT) = 1 ]; then \ 50 | DOCKER_CACHE_ARG="--cache-from ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}upf-epc-$$target:${DOCKER_TAG}"; \ 51 | fi; \ 52 | DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker build $(DOCKER_PULL) $(DOCKER_BUILD_ARGS) \ 53 | --target $$target \ 54 | $$DOCKER_CACHE_ARG \ 55 | --tag ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}upf-epc-$$target:${DOCKER_TAG} \ 56 | --label org.opencontainers.image.source="https://github.com/omec-project/upf-epc" \ 57 | --label org.label.schema.version="${VERSION}" \ 58 | --label org.label.schema.vcs.url="${DOCKER_LABEL_VCS_URL}" \ 59 | --label org.label.schema.vcs.ref="${DOCKER_LABEL_VCS_REF}" \ 60 | --label org.label.schema.build.date="${DOCKER_LABEL_BUILD_DATE}" \ 61 | --label org.opencord.vcs.commit.date="${DOCKER_LABEL_COMMIT_DATE}" \ 62 | . \ 63 | || exit 1; \ 64 | done 65 | 66 | docker-push: 67 | for target in $(DOCKER_TARGETS); do \ 68 | docker push ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}upf-epc-$$target:${DOCKER_TAG}; \ 69 | done 70 | 71 | # Change target to bess-build/pfcpiface to exctract src/obj/bins for performance analysis 72 | output: 73 | DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker build $(DOCKER_PULL) $(DOCKER_BUILD_ARGS) \ 74 | --target artifacts \ 75 | --output type=tar,dest=output.tar \ 76 | .; 77 | rm -rf output && mkdir output && tar -xf output.tar -C output && rm -f output.tar 78 | 79 | test-up4-integration-docker: DOCKER_TARGETS=pfcpiface 80 | test-up4-integration-docker: DOCKER_TAG=integration 81 | test-up4-integration-docker: docker-build 82 | docker rm -f mock-up4 pfcpiface 83 | docker network prune -f 84 | MODE=docker DATAPATH=up4 go test -v -count=1 -failfast -timeout 15m ./test/integration/... 85 | 86 | test-bess-integration-native: 87 | MODE=native DATAPATH=bess go test \ 88 | -v \ 89 | -race \ 90 | -count=1 \ 91 | -failfast \ 92 | ./test/integration/... 93 | 94 | pb: 95 | DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker build $(DOCKER_PULL) $(DOCKER_BUILD_ARGS) \ 96 | --target pb \ 97 | --output output \ 98 | .; 99 | cp -a output/bess_pb ${BESS_PB_DIR} 100 | 101 | # Python grpc/protobuf generation 102 | py-pb: 103 | DOCKER_BUILDKIT=$(DOCKER_BUILDKIT) docker build $(DOCKER_PULL) $(DOCKER_BUILD_ARGS) \ 104 | --target ptf-pb \ 105 | --output output \ 106 | .; 107 | cp -a output/bess_pb/. ${PTF_PB_DIR} 108 | 109 | .coverage: 110 | rm -rf $(CURDIR)/.coverage 111 | mkdir -p $(CURDIR)/.coverage 112 | 113 | test: .coverage 114 | docker run --rm -v $(CURDIR):/upf-epc -w /upf-epc golang:latest \ 115 | go test \ 116 | -race \ 117 | -failfast \ 118 | -coverprofile=.coverage/coverage-unit.txt \ 119 | -covermode=atomic \ 120 | -v \ 121 | ./pfcpiface ./cmd/... 122 | 123 | p4-constants: 124 | $(info *** Generating go constants...) 125 | @docker run --rm -v $(CURDIR):/app -w /app \ 126 | golang:latest go run ./cmd/p4info_code_gen/p4info_code_gen.go \ 127 | -output internal/p4constants/p4constants.go -p4info conf/p4/bin/p4info.txt 128 | @docker run --rm -v $(CURDIR):/app -w /app \ 129 | golang:latest gofmt -w internal/p4constants/p4constants.go 130 | 131 | fmt: 132 | @go fmt ./... 133 | 134 | golint: 135 | @docker run --rm -v $(CURDIR):/app -w /app/pfcpiface golangci/golangci-lint:latest golangci-lint run -v --config /app/.golangci.yml 136 | 137 | check-reuse: 138 | @docker run --rm -v $(CURDIR):/upf-epc -w /upf-epc omecproject/reuse-verify:latest reuse lint 139 | 140 | .PHONY: docker-build docker-push output pb fmt golint check-reuse test-up4-integration .coverage test 141 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.1.1-dev 2 | -------------------------------------------------------------------------------- /VERSION.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /cmd/pfcpiface/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | // Copyright 2022-present Open Networking Foundation 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | 10 | "github.com/omec-project/upf-epc/logger" 11 | "github.com/omec-project/upf-epc/pfcpiface" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | var ( 16 | configPath = flag.String("config", "upf.jsonc", "path to upf config") 17 | ) 18 | 19 | func main() { 20 | // cmdline args 21 | flag.Parse() 22 | 23 | // Read and parse json startup file. 24 | conf, err := pfcpiface.LoadConfigFile(*configPath) 25 | if err != nil { 26 | logger.InitLog.Fatalln("error reading conf file:", err) 27 | } 28 | 29 | lvl, errLevel := zapcore.ParseLevel(conf.LogLevel.String()) 30 | if errLevel != nil { 31 | logger.InitLog.Errorln("can not parse input level") 32 | } 33 | logger.InitLog.Infoln("setting log level to:", lvl) 34 | logger.SetLogLevel(lvl) 35 | 36 | logger.InitLog.Infof("%+v", conf) 37 | 38 | pfcpi := pfcpiface.NewPFCPIface(conf) 39 | 40 | // blocking 41 | pfcpi.Run() 42 | } 43 | -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omec-project/upf/044210cc9dff56fc89aa8b6ff8a17ace19a468b9/conf/__init__.py -------------------------------------------------------------------------------- /conf/closed_loop_svr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-FileCopyrightText: 2025 Intel Corporation 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | import os 6 | import socket 7 | import threading 8 | import logging 9 | import sys 10 | import scapy.all as scapy 11 | from flask import Flask, jsonify 12 | 13 | logging.basicConfig( 14 | level=logging.INFO, 15 | format='%(asctime)s - %(levelname)s - %(message)s', 16 | handlers=[ 17 | logging.StreamHandler(sys.stdout), 18 | logging.StreamHandler(sys.stderr) 19 | ] 20 | ) 21 | 22 | app = Flask(__name__) 23 | 24 | rogueIPs = [] 25 | 26 | @app.route('/', methods=['GET']) 27 | def get_rogueIps(): 28 | logging.info("received request for / endpoint") 29 | response = jsonify({"ipaddresses": rogueIPs}) 30 | rogueIPs.clear() 31 | return response 32 | 33 | def unix_socket_client(socket_path): 34 | client = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) 35 | try: 36 | client.connect(socket_path) 37 | logging.info(f"connected to Unix socket server at {socket_path}") 38 | 39 | while True: 40 | data = client.recv(2048) 41 | if data: 42 | pkt = scapy.Ether(data) 43 | if scapy.IP in pkt: 44 | dst_ip = pkt[scapy.IP].dst 45 | if dst_ip not in rogueIPs: 46 | rogueIPs.append(dst_ip) 47 | logging.info(f"added new rogue IP: {dst_ip}") 48 | else: 49 | break 50 | except Exception as e: 51 | logging.error(f"error connecting to Unix socket server: {e}") 52 | finally: 53 | client.close() 54 | 55 | if __name__ == '__main__': 56 | closed_loop_path = os.getenv('CLOSED_LOOP_SOCKET_PATH', '/tmp/closedloop') 57 | client_thread = threading.Thread(target=unix_socket_client, args=(closed_loop_path,), daemon=True) 58 | client_thread.start() 59 | 60 | port = int(os.getenv('CLOSED_LOOP_PORT', 9301)) 61 | logging.info(f"starting server on port {port}") 62 | app.run(host='0.0.0.0', port=port) 63 | -------------------------------------------------------------------------------- /conf/grafana/dashboards/upf-custom.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020 Intel Corporation 3 | --- 4 | apiVersion: 1 5 | 6 | providers: 7 | - name: 'default' 8 | orgId: 1 9 | folder: '' 10 | type: file 11 | disableDeletion: false 12 | updateIntervalSeconds: 10 13 | options: 14 | path: /var/lib/grafana/dashboards 15 | -------------------------------------------------------------------------------- /conf/grafana/datasources/prometheus.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020 Intel Corporation 3 | --- 4 | apiVersion: 1 5 | 6 | deleteDatasources: 7 | - name: Prometheus 8 | orgId: 1 9 | 10 | datasources: 11 | - name: Prometheus 12 | type: prometheus 13 | orgId: 1 14 | url: http://localhost:9090 15 | version: 1 16 | editable: true 17 | -------------------------------------------------------------------------------- /conf/grafana/gtpu-path-monitoring.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023-present Open Networking Foundation 2 | 3 | SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /conf/grafana/upf-custom.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/grafana/upf-session-summary-latency-heatmap.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/grafana/upf-session-summary.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/grafana/upf-session.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/grafana/upf-session.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/gtp_echo.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2022 Intel Corporation 3 | 4 | import scapy.all as scapy 5 | from scapy.contrib.gtp import * 6 | from scapy.packet import * 7 | 8 | def gtp_echo_request(src_ip): 9 | #use scapy to build a GTP-U Echo Request packet template 10 | eth = scapy.Ether() 11 | ip = scapy.IP(src=src_ip) # dst IP is overwritten 12 | udp = scapy.UDP(sport=2152, dport=2152) 13 | gtp = GTPHeader(gtp_type=1, seq=0) 14 | gtp_echo = GTPEchoRequest() 15 | pkt = eth/ip/udp/gtp/gtp_echo 16 | min_pkt_size = 60 17 | if len(pkt) < min_pkt_size: 18 | pad_len = min_pkt_size - len(pkt) 19 | pad = Padding() 20 | pad.load = '\x00' * pad_len 21 | pkt = pkt/pad 22 | 23 | return bytes(pkt) 24 | -------------------------------------------------------------------------------- /conf/p4/bin/bmv2.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/p4/bin/p4info.txt.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /conf/pktgen.bess: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Intel Corporation 3 | 4 | """ 5 | docker run --name pktgen -td --restart unless-stopped \ 6 | --cpuset-cpus=2-5 --ulimit memlock=-1 --cap-add IPC_LOCK \ 7 | -v /dev/hugepages:/dev/hugepages -v "$PWD/conf":/opt/bess/bessctl/conf \ 8 | --device=/dev/vfio/vfio --device=/dev/vfio/176 \ 9 | upf-epc-bess:"$( Rewrite(templates=n39_pkts) -> n39update::SequentialUpdate(**n3seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=0) 69 | src36::Source(pkt_size=pkt_size) -> Rewrite(templates=n36_pkts) -> n36update::SequentialUpdate(**n3seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=1) 70 | 71 | src9::Source(pkt_size=pkt_size) -> Rewrite(templates=n9_pkts) -> n9update::SequentialUpdate(**n9seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=2) 72 | src6::Source(pkt_size=pkt_size) -> Rewrite(templates=n6_pkts) -> n6update::SequentialUpdate(**n6seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=3) 73 | 74 | src39.attach_task(parent='39_limit') 75 | src36.attach_task(parent='36_limit') 76 | 77 | src9.attach_task(parent='9_limit') 78 | src6.attach_task(parent='6_limit') 79 | -------------------------------------------------------------------------------- /conf/pktgen_cndp.bess: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # Copyright (c) 2020-2022 Intel Corporation 3 | 4 | """ 5 | docker run --name pktgen -td --restart unless-stopped \ 6 | --cpuset-cpus=2-5 --ulimit memlock=-1 --cap-add IPC_LOCK \ 7 | -v /dev/hugepages:/dev/hugepages -v "$PWD/conf":/opt/bess/bessctl/conf \ 8 | -v /lib/firmware/intel:/lib/firmware/intel \ 9 | --device=/dev/vfio/vfio --device=/dev/vfio/119 --device=/dev/vfio/120 \ 10 | upf-epc-bess:"$( Rewrite(templates=n39_pkts) -> n39update::SequentialUpdate(**n3seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=0) 72 | src36::Source(pkt_size=pkt_size) -> Rewrite(templates=n36_pkts) -> n36update::SequentialUpdate(**n3seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p.name, qid=1) 73 | 74 | src9::Source(pkt_size=pkt_size) -> Rewrite(templates=n9_pkts) -> n9update::SequentialUpdate(**n9seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p1.name, qid=0) 75 | src6::Source(pkt_size=pkt_size) -> Rewrite(templates=n6_pkts) -> n6update::SequentialUpdate(**n6seq_kwargs) -> L4Checksum() -> IPChecksum() -> QueueOut(port=p1.name, qid=1) 76 | 77 | src39.attach_task(parent='39_limit') 78 | src36.attach_task(parent='36_limit') 79 | 80 | src9.attach_task(parent='9_limit') 81 | src6.attach_task(parent='6_limit') 82 | -------------------------------------------------------------------------------- /conf/prometheus.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | # SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | # SPDX-License-Identifier: Apache-2.0 4 | scrape_configs: 5 | - job_name: "upf" 6 | static_configs: 7 | - targets: ["localhost:8080"] 8 | -------------------------------------------------------------------------------- /conf/upf.jsonc: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | // SPDX-License-Identifier: Apache-2.0 4 | { 5 | // UPF support various configuration modes: 6 | // "af_packet"` to enable AF_PACKET mode, 7 | // "af_xdp" to enable AF_XDP mode, 8 | // "cndp" to enable CNDP mode, 9 | // "dpdk" to enable DPDK mode, 10 | // "sim" to generate synthetic traffic from BESS's Source module, 11 | // "" when running with UP4 12 | "mode": "dpdk", 13 | 14 | "table_sizes": { 15 | // Example sizes based on sim mode and 50K sessions. Customize as per your control plane 16 | // 50K per unique tuple, we send 4 unique PDR patterns 17 | "pdrLookup": 50000, 18 | // 4 PDRs per session 19 | "flowMeasure": 200000, 20 | // there are 2 QERs and 2 entries per QER 21 | "appQERLookup": 200000, 22 | // there is 1 session QER and 2 entries per session QER 23 | "sessionQERLookup": 100000, 24 | // there are 3 FARs 25 | "farLookup": 150000 26 | }, 27 | 28 | // [Optional] Set the log level to one of "panic", "fatal", "error", "warn", "info", "debug" 29 | "log_level": "info", 30 | 31 | // Use the sim block to enable simulation using either Source module or via il_trafficgen 32 | "sim": { 33 | // At this point we can simulate either N3/N6 or N3/N9 traffic, so choose n6 or n9 below 34 | "core": "n6", 35 | "max_sessions": 50000, 36 | "start_ue_ip": "16.0.0.1", 37 | "start_enb_ip": "11.1.1.129", 38 | "start_aupf_ip": "13.1.1.199", 39 | "n6_app_ip": "6.6.6.6", 40 | "n9_app_ip": "9.9.9.9", 41 | "start_n3_teid": "0x30000000", 42 | "start_n9_teid": "0x90000000", 43 | "uplink_mbr": 500000, 44 | // Make sure uplink_gbr is configured less than uplink_mbr if 'qfi' in pfcpiface/grpcsim.go is for GBR flows. Current default 'qfi' value is for Non-GBR flows 45 | "uplink_gbr": 50000, 46 | "downlink_mbr": 1000000, 47 | // Make sure downlink_gbr is configured less than downlink_mbr if 'qfi' in pfcpiface/grpcsim.go is for GBR flows. Current default 'qfi' value is for Non-GBR flows 48 | "downlink_gbr": 100000, 49 | "pkt_size": 128, 50 | "total_flows": 5000 51 | }, 52 | 53 | // Max IP frag table entries (for IPv4 reassembly). Uncomment line below to enable 54 | // "max_ip_defrag_flows": "1000", 55 | 56 | // Uncomment line below to enable 57 | // "ip_frag_with_eth_mtu": "1518", 58 | 59 | // Enable hardware offload of checksum. Might disable vector PMD 60 | "hwcksum": false, 61 | 62 | // Enable PDU Session Container extension 63 | "gtppsc": false, 64 | 65 | // Enable Intel Dynamic Device Personalization (DDP) 66 | "ddp": false, 67 | 68 | // [Optional] Telemetry-See this link for details: https://github.com/omec-project/bess/blob/master/bessctl/module_tests/timestamp.py 69 | "measure_upf": true, 70 | 71 | // [Optional] Whether to enable flow-level measurements 72 | "measure_flow": false, 73 | 74 | // N3 interface 75 | "access": { 76 | // "cndp_jsonc_file": "conf/cndp_upf_1worker.jsonc", 77 | "ifname": "ens803f2" 78 | }, 79 | 80 | // N6 or N9 interface (depending on the UPF's deployment [PSA-UPF or I-UPF]) 81 | "core": { 82 | // "cndp_jsonc_file": "conf/cndp_upf_1worker.jsonc", 83 | // Uncomment line below to enable UE IP natting. It could be a single IP or multiple IPs 84 | // "ip_masquerade": "18.0.0.1 or 18.0.0.2 or 18.0.0.3", 85 | "ifname": "ens803f3" 86 | }, 87 | 88 | // Number of worker threads. Default: 1 89 | "workers": 1, 90 | 91 | // Parameters for handling outgoing requests 92 | "max_req_retries": 5, 93 | "resp_timeout": "2s", 94 | 95 | // Whether to enable Network Token Functions 96 | "enable_ntf": false, 97 | 98 | // [Optional] Whether to enable End Marker Support 99 | // "enable_end_marker": false, 100 | 101 | // [Optional] Whether to enable Notify BESS feature 102 | // "enable_notify_bess": false, 103 | 104 | // Whether to enable P4Runtime feature 105 | "enable_p4rt": false, 106 | // "conn_timeout": "1000", 107 | // "read_timeout": "25", 108 | // "notify_sockaddr": "/tmp/notifycp", 109 | // "endmarker_sockaddr": "/tmp/pfcpport", 110 | 111 | // Whether to enable UPF HeartBeatTimer feature 112 | "enable_hbTimer": false, 113 | // "heart_beat_interval": "5s", 114 | 115 | // Whether to enable GTPu Path Monitoring 116 | "enable_gtpu_path_monitoring": false, 117 | 118 | "qci_qos_config": [ 119 | { 120 | // Default values for QERs with QCI/QFI not listed below 121 | "qci": 0, 122 | "cbs": 50000, 123 | "ebs": 50000, 124 | "pbs": 50000, 125 | "burst_duration_ms": 10, 126 | "priority": 7 127 | }, 128 | { 129 | "qci": 9, 130 | "cbs": 2048, 131 | "ebs": 2048, 132 | "pbs": 2048, 133 | "priority": 6 134 | }, 135 | { 136 | "qci": 8, 137 | "cbs": 2048, 138 | "ebs": 2048, 139 | "pbs": 2048, 140 | "priority": 5 141 | } 142 | ], 143 | 144 | // [Optional] Slice-wide meter rate limits 145 | "slice_rate_limit_config": { 146 | // Uplink policer 147 | "n6_bps": 500000000, 148 | "n6_burst_bytes": 625000, 149 | // Downlink policer 150 | "n3_bps": 500000000, 151 | "n3_burst_bytes": 625000 152 | }, 153 | 154 | // Control plane controller settings 155 | "cpiface": { 156 | "peers": ["148.162.12.214"], 157 | // [Optional] Below parameters 158 | "dnn": "internet", 159 | "http_port": "8080", 160 | "enable_ue_ip_alloc": false, 161 | // "use_fqdn": "true", 162 | // "hostname": "upf-0", 163 | "ue_ip_pool": "10.250.0.0/16" 164 | }, 165 | 166 | // p4rtc interface settings, 167 | "p4rtciface": { 168 | "access_ip": "172.17.0.1/32", 169 | "p4rtc_server": "onos", 170 | "p4rtc_port": "51001", 171 | // [Optional] Set the UP4 slice identifier that this PFCP Agent instance belongs to. Default: 0 172 | "slice_id": 0, 173 | // [Optional] Default TC is ELASTIC 174 | "default_tc": 3, 175 | // [Optional] Whether to wipe out PFCP state from UP4 datapath on UP4 restart. Default: false 176 | "clear_state_on_restart": false 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /conf/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright 2019 Intel Corporation 4 | 5 | import os 6 | import socket 7 | import struct 8 | import sys 9 | from typing import Optional 10 | 11 | import iptools 12 | from jsoncomment import JsonComment 13 | import psutil 14 | from pyroute2 import NDB 15 | from socket import AF_INET 16 | 17 | 18 | def exit(code, msg): 19 | print(msg) 20 | sys.exit(code) 21 | 22 | 23 | def getpid(process_name): 24 | for proc in psutil.process_iter(attrs=["pid", "name"]): 25 | if process_name == proc.info["name"]: 26 | return proc.info["pid"] 27 | 28 | 29 | def getpythonpid(process_name): 30 | for proc in psutil.process_iter(attrs=["pid", "cmdline"]): 31 | if len(proc.info["cmdline"]) < 2: 32 | continue 33 | if ( 34 | process_name in proc.info["cmdline"][1] 35 | and "python" in proc.info["cmdline"][0] 36 | ): 37 | return proc.info["pid"] 38 | return 39 | 40 | 41 | def get_json_conf(path, dump): 42 | try: 43 | with open(path, 'r') as f: 44 | jsonc_data = f.read() 45 | jc = JsonComment() 46 | conf = jc.loads(jsonc_data) 47 | if dump: 48 | print(jc.dumps(conf, indent=4, sort_keys=True)) 49 | return conf 50 | except Exception as e: 51 | print("An unexpected error occurred:", str(e)) 52 | return None 53 | 54 | 55 | def get_env(varname, default=None): 56 | try: 57 | var = os.environ[varname] 58 | return var 59 | except KeyError: 60 | if default is not None: 61 | return "{}".format(default) 62 | else: 63 | exit(1, "Empty env var {}".format(varname)) 64 | 65 | 66 | def ips_by_interface(name: str) -> list[str]: 67 | ndb = NDB() 68 | interfaces = ndb.interfaces 69 | if iface_record := interfaces.get(name): 70 | for address in iface_record.ipaddr: 71 | if address["family"] == AF_INET: 72 | return [address["local"]] 73 | return [] 74 | 75 | 76 | def atoh(ip): 77 | return socket.inet_aton(ip) 78 | 79 | 80 | def alias_by_interface(name: str) -> Optional[str]: 81 | ndb = NDB() 82 | if iface_record := ndb.interfaces.get(name): 83 | return iface_record["ifalias"] 84 | 85 | 86 | def mac_by_interface(name: str) -> Optional[str]: 87 | ndb = NDB() 88 | if iface_record := ndb.interfaces.get(name): 89 | return iface_record["address"] 90 | 91 | 92 | def mac2hex(mac): 93 | return int(mac.replace(":", ""), 16) 94 | 95 | 96 | def peer_by_interface(name: str) -> str: 97 | ndb = NDB() 98 | try: 99 | peer_idx = ndb.interfaces[name]["link"] 100 | peer_name = ndb.interfaces[peer_idx]["ifname"] 101 | except: 102 | raise Exception("veth interface {} does not exist".format(name)) 103 | else: 104 | return peer_name 105 | 106 | 107 | def aton(ip): 108 | return socket.inet_aton(ip) 109 | 110 | 111 | def validate_cidr(cidr): 112 | return iptools.ipv4.validate_cidr(cidr) 113 | 114 | 115 | def cidr2mask(cidr): 116 | _, prefix = cidr.split("/") 117 | return format(0xFFFFFFFF << (32 - int(prefix)), "08x") 118 | 119 | 120 | def cidr2block(cidr): 121 | return iptools.ipv4.cidr2block(cidr) 122 | 123 | 124 | def ip2hex(ip): 125 | return iptools.ipv4.ip2hex(ip) 126 | 127 | 128 | def cidr2netmask(cidr): 129 | network, net_bits = cidr.split("/") 130 | host_bits = 32 - int(net_bits) 131 | netmask = socket.inet_ntoa(struct.pack("!I", (1 << 32) - (1 << host_bits))) 132 | return network, netmask 133 | 134 | 135 | def ip2long(ip): 136 | return iptools.ipv4.ip2long(ip) 137 | 138 | 139 | def get_process_affinity(): 140 | return psutil.Process().cpu_affinity() 141 | 142 | 143 | def set_process_affinity(pid, cpus): 144 | try: 145 | psutil.Process(pid).cpu_affinity(cpus) 146 | except OSError as e: 147 | # 22 = Invalid argument; PID has PF_NO_SETAFFINITY set 148 | if e.errno == 22: 149 | print(f"Failed to set affinity on process {pid} {psutil.Process(pid).name}") 150 | else: 151 | raise e 152 | 153 | 154 | def set_process_affinity_all(cpus): 155 | for pid in psutil.pids(): 156 | for thread in psutil.Process(pid).threads(): 157 | set_process_affinity(thread.id, cpus) 158 | -------------------------------------------------------------------------------- /deployments/upf-k8s.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2019 Intel Corporation 3 | # Copyright 2020 Open Networking Foundation 4 | --- 5 | apiVersion: "k8s.cni.cncf.io/v1" 6 | kind: NetworkAttachmentDefinition 7 | metadata: 8 | name: access-net 9 | annotations: 10 | k8s.v1.cni.cncf.io/resourceName: intel.com/sriov_vfio_access_net 11 | spec: 12 | config: '{ 13 | "cniVersion": "0.3.1", 14 | "type": "vfioveth", 15 | "name": "access-net", 16 | "ipam": { 17 | "type": "host-local", 18 | "subnet": "198.18.0.0/24", 19 | "rangeStart": "198.18.0.2", 20 | "rangeEnd": "198.18.0.250", 21 | "gateway": "198.18.0.1" 22 | } 23 | }' 24 | --- 25 | apiVersion: "k8s.cni.cncf.io/v1" 26 | kind: NetworkAttachmentDefinition 27 | metadata: 28 | name: core-net 29 | annotations: 30 | k8s.v1.cni.cncf.io/resourceName: intel.com/sriov_vfio_core_net 31 | spec: 32 | config: '{ 33 | "cniVersion": "0.3.1", 34 | "type": "vfioveth", 35 | "name": "core-net", 36 | "ipam": { 37 | "type": "host-local", 38 | "subnet": "198.19.0.0/24", 39 | "rangeStart": "198.19.0.2", 40 | "rangeEnd": "198.19.0.250", 41 | "gateway": "198.19.0.1" 42 | } 43 | }' 44 | --- 45 | apiVersion: v1 46 | kind: ConfigMap 47 | metadata: 48 | name: upf-conf 49 | data: 50 | upf.jsonc: | 51 | { 52 | "access": { 53 | "ifname": "access" 54 | }, 55 | "core": { 56 | "ifname": "core" 57 | }, 58 | "measure_upf": true, 59 | "workers": "1", 60 | "cpiface": { 61 | "dnn": "internet" 62 | } 63 | } 64 | --- 65 | apiVersion: v1 66 | kind: Pod 67 | metadata: 68 | name: upf 69 | labels: 70 | app: upf 71 | annotations: 72 | prometheus.io/scrape: "true" 73 | prometheus.io/port: "8080" 74 | k8s.v1.cni.cncf.io/networks: '[ 75 | { "name": "access-net", "interface": "access" }, 76 | { "name": "core-net", "interface": "core" } 77 | ]' 78 | spec: 79 | shareProcessNamespace: true 80 | initContainers: 81 | # Currently CNI doesn't allow metric we're doing it here instead of net-attach-def 82 | - name: routes 83 | image: omecproject/upf-epc-bess:master-latest 84 | env: 85 | - name: ENB_GNB_SUBNET 86 | value: '11.1.1.128/25' 87 | - name: S1U_N3_GATEWAY 88 | value: '198.18.0.1' 89 | - name: SGI_N6_GATEWAY 90 | value: '198.19.0.1' 91 | command: ["sh", "-xec"] 92 | args: 93 | - ip route add $ENB_GNB_SUBNET via $S1U_N3_GATEWAY; 94 | ip route add default via $SGI_N6_GATEWAY metric 110; 95 | securityContext: 96 | capabilities: 97 | add: 98 | - NET_ADMIN 99 | 100 | # Reqd. if working with AF_PACKET so that kernel does not reply to GTP-U packets 101 | #- name: iptables 102 | # image: omecproject/upf-epc-bess:master-latest 103 | # command: [ "sh", "-xec"] 104 | # args: 105 | # - iptables -I OUTPUT -p icmp --icmp-type port-unreachable -j DROP; 106 | # securityContext: 107 | # capabilities: 108 | # add: 109 | # - NET_ADMIN 110 | containers: 111 | - name: routectl 112 | image: omecproject/upf-epc-bess:master-latest 113 | command: ["/opt/bess/bessctl/conf/route_control.py"] 114 | args: 115 | - -i 116 | - access 117 | - core 118 | env: 119 | - name: PYTHONUNBUFFERED 120 | value: "1" 121 | resources: 122 | limits: 123 | cpu: 256m 124 | memory: 128Mi 125 | - name: bessd 126 | image: omecproject/upf-epc-bess:master-latest 127 | stdin: true 128 | tty: true 129 | args: 130 | - -grpc-url=0.0.0.0:10514 131 | env: 132 | - name: CONF_FILE 133 | value: /conf/upf.jsonc 134 | livenessProbe: 135 | tcpSocket: 136 | port: 10514 137 | initialDelaySeconds: 30 138 | periodSeconds: 20 139 | lifecycle: 140 | postStart: 141 | exec: 142 | command: ["sh", "-c", "until ss | grep -q 10514; do sleep 5; echo waiting for bessd; done; ./bessctl run up4;"] 143 | securityContext: 144 | capabilities: 145 | add: 146 | - IPC_LOCK # AF_PACKET vdev (and 4K pages) uses mmap 147 | resources: 148 | limits: 149 | hugepages-1Gi: 2Gi 150 | cpu: 2 151 | memory: 256Mi 152 | intel.com/sriov_vfio_access_net: '1' 153 | intel.com/sriov_vfio_core_net: '1' 154 | volumeMounts: 155 | - name: upf-conf 156 | mountPath: /conf 157 | - name: hugepages 158 | mountPath: /dev/hugepages 159 | - name: web 160 | image: omecproject/upf-epc-bess:master-latest 161 | command: ["bessctl"] 162 | args: 163 | - http 164 | - 0.0.0.0 165 | - '8000' 166 | resources: 167 | limits: 168 | cpu: 256m 169 | memory: 128Mi 170 | - name: pfcpiface 171 | image: omecproject/upf-epc-pfcpiface:master-latest 172 | command: ["pfcpiface"] 173 | args: 174 | - -config 175 | - /conf/upf.jsonc 176 | volumeMounts: 177 | - name: upf-conf 178 | mountPath: /conf 179 | ports: 180 | - name: http 181 | containerPort: 8080 182 | protocol: TCP 183 | resources: 184 | limits: 185 | cpu: 256m 186 | memory: 128Mi 187 | volumes: 188 | - name: upf-conf 189 | configMap: 190 | name: upf-conf 191 | - name: hugepages 192 | emptyDir: 193 | medium: HugePages 194 | -------------------------------------------------------------------------------- /docs/configuration-guide.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Configuration Guide 7 | 8 | ## PFCP Agent 9 | 10 | This document focuses on frequently used configurations. 11 | 12 | Please refer to [upf.jsonc](../conf/upf.jsonc) file for the full list of configurable parameters. 13 | 14 | ### Common configurations 15 | 16 | These are configurations commonly shared between P4-UPF and BESS-UPF. 17 | 18 | | Config | Default value | Mandatory | Comments | 19 | | ------ | ------------- | --------- | -------- | 20 | | `log_level` | info | No | | 21 | | `hostname` | - | No | Used to get local IP address and local NodeID in PFCP messages | 22 | | `http_port` | 8080 | No | | 23 | | `max_req_retries` | 5 | No | Max retries for sending PFCP message towards SMF/SPGW-C | 24 | | `resp_timeout` | 2s | No | Period to wait for a response from SMF/SPGW-C | 25 | | `enable_end_marker` | false | No | | 26 | | `enable_p4rt` | false | Yes for P4-UPF only | | 27 | | `enable_gtpu_path_monitoring` | false | No | | 28 | | `cpiface.enable_ue_ip_alloc` | false | No | Whether to enable UPF-based UE IP allocation | 29 | | `cpiface.ue_ip_pool` | - | Yes for P4-UPF or when `enable_ue_ip_alloc` is set | IP pool from which we allocate UE IP address | 30 | | `cpiface.dnn` | - | No | Data Network Name to use during PFCP Association | 31 | 32 | ### BESS-UPF specific configurations 33 | 34 | | Config | Default value | Mandatory | Comments | 35 | | ------ | ------------- | --------- | -------- | 36 | | `measure_upf` | false | No | Enable per port metrics | 37 | | `measure_flow` | false | No | Enable per flow metrics | 38 | | `access.ifname` | - | Yes | Access-facing network interface name | 39 | | `core.ifname` | - | Yes | Core-facing network interface name | 40 | | `enable_notify_bess` | false | No | Whether to enable Notify feature for DDNs | 41 | 42 | ### P4-UPF specific configurations 43 | 44 | | Config | Default value | Mandatory | Comments | 45 | | ------ | ------------- | --------- | -------- | 46 | | `p4rtciface.slice_id` | 0 | No | Identify P4-UPF slice this PFCP agent instance belongs to | 47 | | `p4rtciface.access_ip` | - | Yes | N3/S1u address for 5G/4G | 48 | | `p4rtciface.p4rtc_server` | - | Yes | IP address of the P4Runtime server exposed by UP4 | 49 | | `p4rtciface.p4rtc_port` | - | Yes | TCP port of the P4Runtime server exposed by UP4 | 50 | | `p4rtciface.default_tc` | 3 | No | Default Traffic Class (default value is ELASTIC - TC=3) | 51 | | `p4rtciface.clear_state_on_restart` | false | No | Whether to wipe out PFCP state from UP4 datapath on UP4 restart. | -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | 5 | # Developer guide 6 | 7 | ## New Features or Improvements to the BESS pipeline 8 | 9 | When implementing new features or making improvements to the `BESS` pipeline, 10 | the easiest way to do so is by: 11 | 12 | - Clone the `bess` repository inside the UPF repository 13 | ```bash 14 | $ cd 15 | $ git clone https://github.com//bess.git 16 | ``` 17 | 18 | - **Temporarily** modify Dockerfile to use the `bess` cloned in the previous 19 | step 20 | ```diff 21 | diff --git a/Dockerfile b/Dockerfile 22 | index 052456d..03b7d33 100644 23 | --- a/Dockerfile 24 | +++ b/Dockerfile 25 | @@ -11,9 +11,7 @@ RUN apt-get update && \ 26 | 27 | # BESS pre-reqs 28 | WORKDIR /bess 29 | -ARG BESS_COMMIT=master 30 | -RUN git clone https://github.com/omec-project/bess.git . 31 | -RUN git checkout ${BESS_COMMIT} 32 | +COPY bess/ . 33 | RUN cp -a protobuf /protobuf 34 | 35 | # Stage bess-build: builds bess with its dependencies 36 | ``` 37 | 38 | - Implement a feature or make modifications 39 | 40 | - Test the modifications 41 | 42 | - Revert change in Dockerfile 43 | ```diff 44 | diff --git a/Dockerfile b/Dockerfile 45 | index 03b7d33..052456d 100644 46 | --- a/Dockerfile 47 | +++ b/Dockerfile 48 | @@ -11,7 +11,9 @@ RUN apt-get update && \ 49 | 50 | # BESS pre-reqs 51 | WORKDIR /bess 52 | -COPY bess/ . 53 | +ARG BESS_COMMIT=master 54 | +RUN git clone https://github.com/omec-project/bess.git . 55 | +RUN git checkout ${BESS_COMMIT} 56 | RUN cp -a protobuf /protobuf 57 | 58 | # Stage bess-build: builds bess with its dependencies 59 | ``` 60 | 61 | - Commit your changes to `bess` repository and, if needed, `upf` repository 62 | - Open pull request in `bess` repository and, if needed, `upf` repository 63 | 64 | 65 | ## Testing local Go dependencies 66 | 67 | The `upf` repository relies on some external Go dependencies, which are not 68 | mature yet (e.g. pfcpsim or p4runtime-go-client). 69 | It's often needed to extend those dependencies first, before adding a new 70 | feature to the PFCP Agent. However, when using Go modules and containerized 71 | environment, it's hard to test work-in-progress (WIP) changes to local 72 | dependencies. Therefore, this repository comes up with a way to use Go 73 | vendoring, instead of Go modules, for development purposes. 74 | 75 | To use a local Go dependency add the `replace` directive to `go.mod`. An example: 76 | 77 | ``` 78 | replace github.com/antoninbas/p4runtime-go-client v0.0.0-20211006214122-ea704d54a7d3 => ../p4runtime-go-client 79 | ``` 80 | 81 | Then, to build the Docker image using the local dependency: 82 | 83 | ``` 84 | DOCKER_BUILD_ARGS="--build-arg GOFLAGS=-mod=vendor" make docker-build 85 | ``` 86 | 87 | To run E2E integration tests with the local dependency: 88 | 89 | ``` 90 | DOCKER_BUILD_ARGS="--build-arg GOFLAGS=-mod=vendor" make test-up4-integration 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/dpdk-configuration.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # DPDK Configuration 7 | 8 | The following steps are required to properly configure the devices to deploy the 9 | UPF in DPDK mode. Let's assume that interfaces `ens801f0` and `ens801f1` are the 10 | ones to be used for this purpose. 11 | 12 | - Get their MAC addresses 13 | ```bash 14 | $ ip a 15 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 16 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 17 | inet 127.0.0.1/8 scope host lo 18 | valid_lft forever preferred_lft forever 19 | inet6 ::1/128 scope host 20 | valid_lft forever preferred_lft forever 21 | ... 22 | 3: ens801f0: mtu 1500 qdisc mq state UP group default qlen 1000 23 | link/ether b4:96:91:b1:ff:f0 brd ff:ff:ff:ff:ff:ff 24 | 4: ens801f1: mtu 1500 qdisc mq state UP group default qlen 1000 25 | link/ether b4:96:91:b1:ff:f1 brd ff:ff:ff:ff:ff:ff 26 | ... 27 | ``` 28 | 29 | - Download a copy of dpdk-devbind script 30 | 31 | The dpdk-devbind script from DPDK is used for this purpose. To get a copy of it, 32 | execute the following command from the UPF's root directory: 33 | ```bash 34 | $ wget https://raw.githubusercontent.com/DPDK/dpdk/main/usertools/dpdk-devbind.py -O dpdk-devbind.py 35 | $ chmod +x dpdk-devbind.py 36 | ``` 37 | 38 | - Get the PCI addresses of interest 39 | ```bash 40 | $ ./dpdk-devbind.py -s 41 | Network devices using kernel driver 42 | =================================== 43 | 0000:17:00.0 'Ethernet Controller X710 for 10GBASE-T 15ff' if=ens260f0 drv=i40e unused=vfio-pci *Active* 44 | 0000:17:00.1 'Ethernet Controller X710 for 10GBASE-T 15ff' if=ens260f1 drv=i40e unused=vfio-pci 45 | 0000:4b:00.0 'Ethernet Controller E810-C for QSFP 1592' if=ens785f0 drv=ice unused=vfio-pci 46 | 0000:4b:00.1 'Ethernet Controller E810-C for QSFP 1592' if=ens785f1 drv=ice unused=vfio-pci 47 | 0000:b1:00.0 'Ethernet Controller E810-C for QSFP 1592' if=ens801f0 drv=ice unused=vfio-pci 48 | 0000:b1:00.1 'Ethernet Controller E810-C for QSFP 1592' if=ens801f1 drv=ice unused=vfio-pci 49 | 50 | No 'Baseband' devices detected 51 | ============================== 52 | 53 | ... 54 | ``` 55 | 56 | - Bind devices to `DPDK-compatible driver` 57 | 58 | ```bash 59 | $ sudo ./dpdk-devbind.py -b vfio-pci 0000:b1:00.0 60 | $ sudo ./dpdk-devbind.py -b vfio-pci 0000:b1:00.1 61 | ``` 62 | 63 | - Verify that the binding was successful 64 | ```bash 65 | $ ./dpdk-devbind.py -s 66 | 67 | Network devices using DPDK-compatible driver 68 | ============================================ 69 | 0000:b1:00.0 'Ethernet Controller E810-C for QSFP 1592' drv=vfio-pci unused=ice 70 | 0000:b1:00.1 'Ethernet Controller E810-C for QSFP 1592' drv=vfio-pci unused=ice 71 | 72 | Network devices using kernel driver 73 | =================================== 74 | 0000:17:00.0 'Ethernet Controller X710 for 10GBASE-T 15ff' if=ens260f0 drv=i40e unused=vfio-pci *Active* 75 | 0000:17:00.1 'Ethernet Controller X710 for 10GBASE-T 15ff' if=ens260f1 drv=i40e unused=vfio-pci 76 | 0000:4b:00.0 'Ethernet Controller E810-C for QSFP 1592' if=ens785f0 drv=ice unused=vfio-pci 77 | 0000:4b:00.1 'Ethernet Controller E810-C for QSFP 1592' if=ens785f1 drv=ice unused=vfio-pci 78 | 79 | No 'Baseband' devices detected 80 | ============================== 81 | 82 | ... 83 | ``` 84 | 85 | - Now, check the group that these two interfaces got assigned 86 | ```bash 87 | $ ls /dev/vfio/ 88 | 184 185 vfio 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/images/bess-programming.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/images/bess-upf-on-ec2.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/images/cndp-omec-upf-test-setup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omec-project/upf/044210cc9dff56fc89aa8b6ff8a17ace19a468b9/docs/images/cndp-omec-upf-test-setup.jpg -------------------------------------------------------------------------------- /docs/images/cndp-omec-upf-test-setup.jpg.license: -------------------------------------------------------------------------------- 1 | Copyright 2019 Intel Corporation 2 | 3 | SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /docs/images/pipeline.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/images/ubench-pktgen.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/images/ubench-sim.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/images/upf-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omec-project/upf/044210cc9dff56fc89aa8b6ff8a17ace19a468b9/docs/images/upf-overview.jpg -------------------------------------------------------------------------------- /docs/images/upf-overview.jpg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022-present Open Networking Foundation 2 | 3 | SPDX-License-Identifier: Apache-2.0 4 | -------------------------------------------------------------------------------- /docs/images/upf.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /docs/pipeline.txt.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/omec-project/upf-epc 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/Showmax/go-fqdn v1.0.0 7 | github.com/antoninbas/p4runtime-go-client v0.0.0-20220204221603-49eba9f248c1 8 | github.com/deckarep/golang-set v1.8.0 9 | github.com/docker/docker v26.1.5+incompatible 10 | github.com/docker/go-connections v0.4.0 11 | github.com/ettle/strcase v0.2.0 12 | github.com/golang/protobuf v1.5.4 13 | github.com/google/gopacket v1.1.19 14 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 15 | github.com/libp2p/go-reuseport v0.1.0 16 | github.com/omec-project/pfcpsim v1.2.2 17 | github.com/p4lang/p4runtime v1.3.0 18 | github.com/prometheus/client_golang v1.11.1 19 | github.com/stretchr/testify v1.10.0 20 | github.com/wmnsk/go-pfcp v0.0.24 21 | go.uber.org/zap v1.27.0 22 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 23 | google.golang.org/grpc v1.71.0 24 | google.golang.org/protobuf v1.36.5 25 | ) 26 | 27 | require ( 28 | github.com/Microsoft/go-winio v0.5.2 // indirect 29 | github.com/beorn7/perks v1.0.1 // indirect 30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 | github.com/containerd/log v0.1.0 // indirect 32 | github.com/davecgh/go-spew v1.1.1 // indirect 33 | github.com/distribution/reference v0.6.0 // indirect 34 | github.com/docker/go-units v0.4.0 // indirect 35 | github.com/felixge/httpsnoop v1.0.4 // indirect 36 | github.com/go-logr/logr v1.4.2 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 40 | github.com/moby/docker-image-spec v1.3.1 // indirect 41 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 42 | github.com/morikuni/aec v1.0.0 // indirect 43 | github.com/opencontainers/go-digest v1.0.0 // indirect 44 | github.com/opencontainers/image-spec v1.0.2 // indirect 45 | github.com/pkg/errors v0.9.1 // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | github.com/prometheus/client_model v0.2.0 // indirect 48 | github.com/prometheus/common v0.26.0 // indirect 49 | github.com/prometheus/procfs v0.6.0 // indirect 50 | github.com/sirupsen/logrus v1.9.3 // indirect 51 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 52 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 53 | go.opentelemetry.io/otel v1.34.0 // indirect 54 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect 55 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 56 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 57 | go.uber.org/multierr v1.10.0 // indirect 58 | golang.org/x/net v0.38.0 // indirect 59 | golang.org/x/sys v0.31.0 // indirect 60 | golang.org/x/text v0.23.0 // indirect 61 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect 62 | gopkg.in/yaml.v3 v3.0.1 // indirect 63 | gotest.tools/v3 v3.1.0 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /go.mod.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /go.sum.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Intel Corporation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package logger 6 | 7 | import ( 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | var ( 13 | log *zap.Logger 14 | BessLog *zap.SugaredLogger 15 | DockerLog *zap.SugaredLogger 16 | InitLog *zap.SugaredLogger 17 | P4Log *zap.SugaredLogger 18 | PfcpLog *zap.SugaredLogger 19 | atomicLevel zap.AtomicLevel 20 | ) 21 | 22 | func init() { 23 | atomicLevel = zap.NewAtomicLevelAt(zap.InfoLevel) 24 | config := zap.Config{ 25 | Level: atomicLevel, 26 | Development: false, 27 | Encoding: "console", 28 | EncoderConfig: zap.NewProductionEncoderConfig(), 29 | OutputPaths: []string{"stdout"}, 30 | ErrorOutputPaths: []string{"stderr"}, 31 | } 32 | 33 | config.EncoderConfig.TimeKey = "timestamp" 34 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 35 | config.EncoderConfig.LevelKey = "level" 36 | config.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 37 | config.EncoderConfig.CallerKey = "caller" 38 | config.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 39 | config.EncoderConfig.MessageKey = "message" 40 | config.EncoderConfig.StacktraceKey = "" 41 | 42 | var err error 43 | log, err = config.Build() 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | BessLog = log.Sugar().With("component", "UPF", "category", "BESS") 49 | DockerLog = log.Sugar().With("component", "UPF", "category", "Docker") 50 | InitLog = log.Sugar().With("component", "UPF", "category", "Init") 51 | P4Log = log.Sugar().With("component", "UPF", "category", "P4") 52 | PfcpLog = log.Sugar().With("component", "UPF", "category", "Pfcp") 53 | } 54 | 55 | func GetLogger() *zap.Logger { 56 | return log 57 | } 58 | 59 | // SetLogLevel: set the log level (panic|fatal|error|warn|info|debug) 60 | func SetLogLevel(level zapcore.Level) { 61 | InitLog.Infoln("set log level:", level) 62 | atomicLevel.SetLevel(level) 63 | } 64 | -------------------------------------------------------------------------------- /pfcpiface/config_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "io/fs" 8 | "os" 9 | "testing" 10 | 11 | "go.uber.org/zap" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func mustWriteStringToDisk(s string, path string) { 18 | err := os.WriteFile(path, []byte(s), fs.ModePerm) 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func TestLoadConfigFile(t *testing.T) { 25 | t.Run("sample config is valid", func(t *testing.T) { 26 | s := `{ 27 | "mode": "dpdk", 28 | "log_level": "info", 29 | "workers": 1, 30 | "max_sessions": 50000, 31 | "table_sizes": { 32 | "pdrLookup": 50000, 33 | "appQERLookup": 200000, 34 | "sessionQERLookup": 100000, 35 | "farLookup": 150000 36 | }, 37 | "access": { 38 | "ifname": "access" 39 | }, 40 | "core": { 41 | "ifname": "core" 42 | }, 43 | "measure_upf": true, 44 | "measure_flow": true, 45 | "enable_notify_bess": true, 46 | "notify_sockaddr": "/pod-share/notifycp", 47 | "cpiface": { 48 | "dnn": "internet", 49 | "hostname": "upf", 50 | "http_port": "8080" 51 | }, 52 | "n6_bps": 1000000000, 53 | "n6_burst_bytes": 12500000, 54 | "n3_bps": 1000000000, 55 | "n3_burst_bytes": 12500000, 56 | "qci_qos_config": [{ 57 | "qci": 0, 58 | "cbs": 50000, 59 | "ebs": 50000, 60 | "pbs": 50000, 61 | "burst_duration_ms": 10, 62 | "priority": 7 63 | }] 64 | }` 65 | confPath := t.TempDir() + "/conf.jsonc" 66 | mustWriteStringToDisk(s, confPath) 67 | 68 | _, err := LoadConfigFile(confPath) 69 | require.NoError(t, err) 70 | }) 71 | 72 | t.Run("empty config has log level info", func(t *testing.T) { 73 | s := `{ 74 | "mode": "dpdk" 75 | }` 76 | confPath := t.TempDir() + "/conf.jsonc" 77 | mustWriteStringToDisk(s, confPath) 78 | 79 | conf, err := LoadConfigFile(confPath) 80 | require.NoError(t, err) 81 | require.Equal(t, conf.LogLevel, zap.InfoLevel) 82 | }) 83 | 84 | t.Run("all sample configs must be valid", func(t *testing.T) { 85 | paths := []string{ 86 | "../conf/upf.jsonc", 87 | "../ptf/config/upf.jsonc", 88 | } 89 | 90 | for _, path := range paths { 91 | _, err := LoadConfigFile(path) 92 | assert.NoError(t, err, "config %v is not valid", path) 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /pfcpiface/datapath.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020-present Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | type upfMsgType int 13 | 14 | const ( 15 | upfMsgTypeAdd upfMsgType = iota 16 | upfMsgTypeMod 17 | upfMsgTypeDel 18 | upfMsgTypeClear 19 | ) 20 | 21 | func (u upfMsgType) String() string { 22 | switch u { 23 | case upfMsgTypeAdd: 24 | return "add" 25 | case upfMsgTypeMod: 26 | return "modify" 27 | case upfMsgTypeDel: 28 | return "delete" //nolint 29 | case upfMsgTypeClear: 30 | return "clear" 31 | default: 32 | return "unknown" 33 | } 34 | } 35 | 36 | type datapath interface { 37 | /* Close any pending sessions */ 38 | Exit() 39 | /* setup internal parameters and channel with datapath */ 40 | SetUpfInfo(u *upf, conf *Conf) 41 | /* set up slice info */ 42 | AddSliceInfo(sliceInfo *SliceInfo) error 43 | /* write endMarker to datapath */ 44 | SendEndMarkers(endMarkerList *[][]byte) error 45 | /* write pdr/far/qer to datapath */ 46 | // "master" function to send create/update/delete messages to UPF. 47 | // "newRules" PacketForwardingRules are only used for update messages to UPF. 48 | // TODO: we should have better CRUD API, with a single function per message type. 49 | SendMsgToUPF(method upfMsgType, all PacketForwardingRules, newRules PacketForwardingRules) uint8 50 | /* check of communication channel to datapath is setup */ 51 | IsConnected(accessIP *net.IP) bool 52 | SummaryLatencyJitter(uc *upfCollector, ch chan<- prometheus.Metric) 53 | PortStats(uc *upfCollector, ch chan<- prometheus.Metric) 54 | SummaryGtpuLatency(uc *upfCollector, ch chan<- prometheus.Metric) 55 | SessionStats(pc *PfcpNodeCollector, ch chan<- prometheus.Metric) error 56 | } 57 | -------------------------------------------------------------------------------- /pfcpiface/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021 Open Networking Foundation 3 | package pfcpiface 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | var ( 11 | errNotFound = errors.New("not found") 12 | errInvalidArgument = errors.New("invalid argument") 13 | errInvalidOperation = errors.New("invalid operation") 14 | errFailed = errors.New("failed") 15 | errUnsupported = errors.New("unsupported") 16 | ) 17 | 18 | func ErrUnsupported(what string, value interface{}) error { 19 | return fmt.Errorf("%s=%v %w", what, value, errUnsupported) 20 | } 21 | 22 | func ErrNotFound(what string) error { 23 | return fmt.Errorf("%s %w", what, errNotFound) 24 | } 25 | 26 | func ErrNotFoundWithParam(what string, paramName string, paramValue interface{}) error { 27 | return fmt.Errorf("%s %w with %s=%v", what, errNotFound, paramName, paramValue) 28 | } 29 | 30 | func ErrInvalidOperation(operation interface{}) error { 31 | return fmt.Errorf("%w: %v", errInvalidOperation, operation) 32 | } 33 | 34 | func ErrInvalidArgument(name string, value interface{}) error { 35 | return fmt.Errorf("%w '%s': %v", errInvalidArgument, name, value) 36 | } 37 | 38 | func ErrInvalidArgumentWithReason(name string, value interface{}, reason string) error { 39 | return fmt.Errorf("%w '%s'=%v (%s)", errInvalidArgument, name, value, reason) 40 | } 41 | 42 | func ErrOperationFailedWithReason(operation interface{}, reason string) error { 43 | return fmt.Errorf("%v %w due to: : %s", operation, errFailed, reason) 44 | } 45 | 46 | func ErrOperationFailedWithParam(operation interface{}, paramName string, paramValue interface{}) error { 47 | return fmt.Errorf("'%v' %w for %s=%v", operation, errFailed, paramName, paramValue) 48 | } 49 | -------------------------------------------------------------------------------- /pfcpiface/fteid.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2024 Canonical Ltd. 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "errors" 8 | "math" 9 | "sync" 10 | ) 11 | 12 | const ( 13 | minValue = 1 14 | maxValue = math.MaxUint32 15 | ) 16 | 17 | type FTEIDGenerator struct { 18 | lock sync.Mutex 19 | offset uint32 20 | usedMap map[uint32]bool 21 | } 22 | 23 | func NewFTEIDGenerator() *FTEIDGenerator { 24 | return &FTEIDGenerator{ 25 | offset: 0, 26 | usedMap: make(map[uint32]bool), 27 | } 28 | } 29 | 30 | // Allocate and return an id in range [minValue, maxValue] 31 | func (idGenerator *FTEIDGenerator) Allocate() (uint32, error) { 32 | idGenerator.lock.Lock() 33 | defer idGenerator.lock.Unlock() 34 | 35 | offsetBegin := idGenerator.offset 36 | for { 37 | if _, ok := idGenerator.usedMap[idGenerator.offset]; ok { 38 | idGenerator.updateOffset() 39 | 40 | if idGenerator.offset == offsetBegin { 41 | return 0, errors.New("no available value range to allocate id") 42 | } 43 | } else { 44 | break 45 | } 46 | } 47 | idGenerator.usedMap[idGenerator.offset] = true 48 | id := idGenerator.offset + minValue 49 | idGenerator.updateOffset() 50 | return id, nil 51 | } 52 | 53 | func (idGenerator *FTEIDGenerator) FreeID(id uint32) { 54 | if id < minValue { 55 | return 56 | } 57 | idGenerator.lock.Lock() 58 | defer idGenerator.lock.Unlock() 59 | delete(idGenerator.usedMap, id-minValue) 60 | } 61 | 62 | func (idGenerator *FTEIDGenerator) IsAllocated(id uint32) bool { 63 | if id < minValue { 64 | return false 65 | } 66 | idGenerator.lock.Lock() 67 | defer idGenerator.lock.Unlock() 68 | _, ok := idGenerator.usedMap[id-minValue] 69 | return ok 70 | } 71 | 72 | func (idGenerator *FTEIDGenerator) updateOffset() { 73 | idGenerator.offset++ 74 | idGenerator.offset = idGenerator.offset % maxValue 75 | } 76 | -------------------------------------------------------------------------------- /pfcpiface/fteid_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2024 Canonical Ltd. 3 | 4 | package pfcpiface_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/omec-project/upf-epc/pfcpiface" 10 | ) 11 | 12 | func TestFTEIDAllocate(t *testing.T) { 13 | fteidGenerator := pfcpiface.NewFTEIDGenerator() 14 | 15 | fteid, err := fteidGenerator.Allocate() 16 | if err != nil { 17 | t.Errorf("FTEID allocation failed: %v", err) 18 | } 19 | if fteid < 1 { 20 | t.Errorf("FTEID allocation failed, value is too small: %v", fteid) 21 | } 22 | if !fteidGenerator.IsAllocated(fteid) { 23 | t.Errorf("FTEID was not allocated") 24 | } 25 | } 26 | 27 | func TestFTEIDFree(t *testing.T) { 28 | fteidGenerator := pfcpiface.NewFTEIDGenerator() 29 | fteid, err := fteidGenerator.Allocate() 30 | if err != nil { 31 | t.Errorf("FTEID allocation failed: %v", err) 32 | } 33 | 34 | fteidGenerator.FreeID(fteid) 35 | 36 | if fteidGenerator.IsAllocated(fteid) { 37 | t.Errorf("FTEID was not freed") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pfcpiface/ip_pool.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/omec-project/upf-epc/logger" 13 | ) 14 | 15 | type IPPool struct { 16 | mu sync.Mutex 17 | freePool []net.IP 18 | // inventory keeps track of allocated sessions and their IPs. 19 | inventory map[uint64]net.IP 20 | } 21 | 22 | // NewIPPool creates a new pool of IP addresses with the given subnet. 23 | // The smallest supported size is a /30. 24 | func NewIPPool(poolSubnet string) (*IPPool, error) { 25 | ip, ipnet, err := net.ParseCIDR(poolSubnet) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | i := &IPPool{ 31 | inventory: make(map[uint64]net.IP), 32 | } 33 | 34 | for ip = ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { 35 | ipVal := make(net.IP, len(ip)) 36 | copy(ipVal, ip) 37 | i.freePool = append(i.freePool, ipVal) 38 | } 39 | 40 | if len(i.freePool) < 2 { 41 | return nil, ErrInvalidArgumentWithReason("NewIPPool", poolSubnet, "pool subnet is too small to use as a pool") 42 | } 43 | 44 | // Remove network address and broadcast address. 45 | i.freePool = i.freePool[1 : len(i.freePool)-1] 46 | 47 | return i, nil 48 | } 49 | 50 | func (i *IPPool) LookupOrAllocIP(seid uint64) (net.IP, error) { 51 | i.mu.Lock() 52 | defer i.mu.Unlock() 53 | 54 | // Try to find an exiting session and return the allocated IP. 55 | ip, found := i.inventory[seid] 56 | if found { 57 | logger.PfcpLog.Debugln("found existing session", seid, "IP", ip) 58 | return ip, nil 59 | } 60 | 61 | // Check capacity before new allocations. 62 | if len(i.freePool) == 0 { 63 | return nil, ErrOperationFailedWithReason("IP allocation", "ip pool empty") 64 | } 65 | 66 | ip = i.freePool[0] 67 | i.freePool = i.freePool[1:] // Slice off the element once it is dequeued. 68 | i.inventory[seid] = ip 69 | logger.PfcpLog.Debugln("allocated new session", seid, "IP", ip) 70 | 71 | ipVal := make(net.IP, len(ip)) 72 | copy(ipVal, ip) 73 | 74 | return ipVal, nil 75 | } 76 | 77 | func (i *IPPool) DeallocIP(seid uint64) error { 78 | i.mu.Lock() 79 | defer i.mu.Unlock() 80 | 81 | ip, ok := i.inventory[seid] 82 | if !ok { 83 | logger.PfcpLog.Warnln("attempt to dealloc non-existent session", seid) 84 | return ErrInvalidArgumentWithReason("seid", seid, "can't dealloc non-existent session") 85 | } 86 | 87 | delete(i.inventory, seid) 88 | i.freePool = append(i.freePool, ip) // Simply append to enqueue. 89 | logger.PfcpLog.Debugln("deallocated session", seid, "IP", ip) 90 | 91 | return nil 92 | } 93 | 94 | func (i *IPPool) String() string { 95 | i.mu.Lock() 96 | defer i.mu.Unlock() 97 | 98 | sb := strings.Builder{} 99 | sb.WriteString("inventory: ") 100 | 101 | for s, e := range i.inventory { 102 | sb.WriteString(fmt.Sprintf("{F-SEID %v -> %+v} ", s, e)) 103 | } 104 | 105 | sb.WriteString(fmt.Sprintf("Number of free IP addresses left: %d", len(i.freePool))) 106 | 107 | return sb.String() 108 | } 109 | -------------------------------------------------------------------------------- /pfcpiface/ip_pool_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "math" 11 | "net" 12 | "sync" 13 | "testing" 14 | ) 15 | 16 | func TestNewIPPool(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | poolSubnet string 20 | wantErr bool 21 | }{ 22 | {name: "normal pool", poolSubnet: "10.0.0.0/24", wantErr: false}, 23 | {name: "smallest allowed pool", poolSubnet: "10.0.0.0/30", wantErr: false}, 24 | {name: "IPv6 pool", poolSubnet: "2001::/124", wantErr: false}, 25 | {name: "too small pool", poolSubnet: "10.0.0.0/32", wantErr: true}, 26 | {name: "missing subnet", poolSubnet: "", wantErr: true}, 27 | {name: "invalid subnet", poolSubnet: "foobar", wantErr: true}, 28 | } 29 | 30 | for _, tt := range tests { 31 | t.Run( 32 | tt.name, func(t *testing.T) { 33 | _, err := NewIPPool(tt.poolSubnet) 34 | if !tt.wantErr { 35 | require.NoError(t, err) 36 | } else { 37 | require.Error(t, err) 38 | } 39 | }, 40 | ) 41 | } 42 | } 43 | 44 | func TestIPPool_LookupOrAllocIP(t *testing.T) { 45 | t.Run("allocation in IPv6 subnet", func(t *testing.T) { 46 | const poolSubnet = "2001::/124" 47 | const seid = 1234 48 | pool, err := NewIPPool(poolSubnet) 49 | require.NoError(t, err) 50 | ip, err := pool.LookupOrAllocIP(seid) 51 | require.NoError(t, err) 52 | require.Len(t, ip, net.IPv6len) 53 | }) 54 | 55 | t.Run("repeated SEID lookups return same IP", func(t *testing.T) { 56 | const poolSubnet = "10.0.0.0/24" 57 | const seid = 1234 58 | pool, err := NewIPPool(poolSubnet) 59 | require.NoError(t, err) 60 | ip1, err := pool.LookupOrAllocIP(seid) 61 | require.NoError(t, err) 62 | ip2, err := pool.LookupOrAllocIP(seid) 63 | require.NoError(t, err) 64 | require.Equal(t, ip2, ip1) 65 | require.Len(t, ip1, net.IPv4len) 66 | }) 67 | 68 | t.Run("full subnet allocation", func(t *testing.T) { 69 | const poolSubnet = "10.0.0.0/24" 70 | const usableAddresses = 256 - 2 // Account for network and broadcast addresses 71 | const baseSeid = 1000 72 | _, ipnet, err := net.ParseCIDR(poolSubnet) 73 | require.NoError(t, err) 74 | pool, err := NewIPPool(poolSubnet) 75 | require.NoError(t, err) 76 | seidToIpMap := map[uint64]net.IP{} 77 | var ip net.IP 78 | for i := uint64(0); i < usableAddresses; i++ { 79 | ip, err = pool.LookupOrAllocIP(baseSeid + i) 80 | require.NoError(t, err) 81 | seidToIpMap[baseSeid+i] = ip 82 | } 83 | 84 | for _, ip := range seidToIpMap { 85 | require.True(t, ipnet.Contains(ip), "allocated ip %v not in subnet %v", ip, ipnet) 86 | } 87 | 88 | _, err = pool.LookupOrAllocIP(baseSeid + usableAddresses + 1) 89 | require.Error(t, err, "ip alloc should fail after subnet has been exhausted") 90 | 91 | for seid, ip := range seidToIpMap { 92 | lookupIP, err := pool.LookupOrAllocIP(seid) 93 | require.NoError(t, err, "already allocated IPs must be still be lookup-able") 94 | require.Equal(t, ip, lookupIP, "looked up IP for SEID %v changed", seid) 95 | } 96 | }) 97 | 98 | t.Run("concurrent allocation", func(t *testing.T) { 99 | const workers = 4 100 | const seidsPerWorker = 5000 101 | pool, err := NewIPPool("10.0.0.0/16") 102 | require.NoError(t, err) 103 | wg := sync.WaitGroup{} 104 | worker := func(startSeid uint64) { 105 | for seid := startSeid; seid < startSeid+seidsPerWorker; seid++ { 106 | _, err := pool.LookupOrAllocIP(seid) 107 | require.NoError(t, err) 108 | } 109 | wg.Done() 110 | } 111 | for i := uint64(0); i < workers; i++ { 112 | wg.Add(1) 113 | go worker(i * seidsPerWorker) 114 | } 115 | wg.Wait() 116 | }) 117 | } 118 | 119 | func TestIPPool_DeallocIP(t *testing.T) { 120 | t.Run("plain alloc into dealloc", func(t *testing.T) { 121 | const poolSubnet = "10.0.0.0/24" 122 | const seid = 1234 123 | pool, err := NewIPPool(poolSubnet) 124 | require.NoError(t, err) 125 | _, err = pool.LookupOrAllocIP(seid) 126 | require.NoError(t, err) 127 | err = pool.DeallocIP(seid) 128 | require.NoError(t, err) 129 | }) 130 | 131 | t.Run("dealloc non-existent SEIDs fails", func(t *testing.T) { 132 | pool, err := NewIPPool("10.0.0.0/24") 133 | require.NoError(t, err) 134 | err = pool.DeallocIP(1234) 135 | assert.Error(t, err) 136 | err = pool.DeallocIP(0) 137 | assert.Error(t, err) 138 | err = pool.DeallocIP(math.MaxUint64) 139 | assert.Error(t, err) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /pfcpiface/local_store.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "sync" 8 | 9 | "github.com/omec-project/upf-epc/logger" 10 | ) 11 | 12 | type InMemoryStore struct { 13 | // sessions stores all PFCP sessions. 14 | // sync.Map is optimized for case when multiple goroutines 15 | // read, write, and overwrite entries for disjoint sets of keys. 16 | sessions sync.Map 17 | } 18 | 19 | func NewInMemoryStore() *InMemoryStore { 20 | return &InMemoryStore{} 21 | } 22 | 23 | func (i *InMemoryStore) GetAllSessions() []PFCPSession { 24 | sessions := make([]PFCPSession, 0) 25 | 26 | i.sessions.Range(func(key, value interface{}) bool { 27 | v := value.(PFCPSession) 28 | sessions = append(sessions, v) 29 | return true 30 | }) 31 | 32 | logger.PfcpLog.With("sessions", sessions).Debugln("got all PFCP sessions from local store") 33 | 34 | return sessions 35 | } 36 | 37 | func (i *InMemoryStore) PutSession(session PFCPSession) error { 38 | if session.localSEID == 0 { 39 | return ErrInvalidArgument("session.localSEID", session.localSEID) 40 | } 41 | 42 | i.sessions.Store(session.localSEID, session) 43 | 44 | logger.PfcpLog.With("session", session).Debugln("saved PFCP sessions to local store") 45 | 46 | return nil 47 | } 48 | 49 | func (i *InMemoryStore) DeleteSession(fseid uint64) error { 50 | i.sessions.Delete(fseid) 51 | 52 | logger.PfcpLog.With("F-SEID", fseid).Debugln("PFCP session removed from local store") 53 | 54 | return nil 55 | } 56 | 57 | func (i *InMemoryStore) DeleteAllSessions() bool { 58 | i.sessions.Range(func(key, value interface{}) bool { 59 | i.sessions.Delete(key) 60 | return true 61 | }) 62 | 63 | logger.P4Log.Debugln("all PFCP sessions removed from local store") 64 | 65 | return true 66 | } 67 | 68 | func (i *InMemoryStore) GetSession(fseid uint64) (PFCPSession, bool) { 69 | sess, ok := i.sessions.Load(fseid) 70 | if !ok { 71 | return PFCPSession{}, false 72 | } 73 | 74 | session, ok := sess.(PFCPSession) 75 | if !ok { 76 | return PFCPSession{}, false 77 | } 78 | 79 | logger.PfcpLog.With("session", session).Debugln("Got PFCP session from local store") 80 | 81 | return session, ok 82 | } 83 | -------------------------------------------------------------------------------- /pfcpiface/messages.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "errors" 8 | "time" 9 | 10 | "github.com/wmnsk/go-pfcp/message" 11 | 12 | "github.com/omec-project/upf-epc/logger" 13 | "github.com/omec-project/upf-epc/pfcpiface/metrics" 14 | ) 15 | 16 | var errMsgUnexpectedType = errors.New("unable to parse message as type specified") 17 | 18 | type HandlePFCPMsgError struct { 19 | Op string 20 | Err error 21 | } 22 | 23 | func (e *HandlePFCPMsgError) Error() string { 24 | return "Error during " + e.Op + ": " + e.Err.Error() 25 | } 26 | 27 | func errUnmarshal(err error) *HandlePFCPMsgError { 28 | return &HandlePFCPMsgError{Op: "Unmarshal", Err: err} 29 | } 30 | 31 | func errProcess(err error) *HandlePFCPMsgError { 32 | return &HandlePFCPMsgError{Op: "Process", Err: err} 33 | } 34 | 35 | type Request struct { 36 | msg message.Message // Request message 37 | reply chan message.Message // Response message 38 | } 39 | 40 | func newRequest(msg message.Message) *Request { 41 | return &Request{msg: msg, reply: make(chan message.Message)} 42 | } 43 | 44 | func (r *Request) GetResponse(done <-chan struct{}, respDuration time.Duration) (message.Message, bool) { 45 | respTimer := time.NewTimer(respDuration) 46 | select { 47 | case <-done: 48 | return nil, false 49 | case c := <-r.reply: 50 | respTimer.Stop() 51 | return c, false 52 | case <-respTimer.C: 53 | return nil, true 54 | } 55 | } 56 | 57 | // HandlePFCPMsg handles different types of PFCP messages. 58 | func (pConn *PFCPConn) HandlePFCPMsg(buf []byte) { 59 | var ( 60 | reply message.Message 61 | err error 62 | ) 63 | 64 | msg, err := message.Parse(buf) 65 | if err != nil { 66 | logger.PfcpLog.Errorf("ignoring undecodable message: %v, error: %v", buf, err) 67 | return 68 | } 69 | 70 | addr := pConn.RemoteAddr().String() 71 | msgType := msg.MessageTypeName() 72 | m := metrics.NewMessage(msgType, "Incoming") 73 | 74 | switch msg.MessageType() { 75 | // Connection related messages 76 | case message.MsgTypeHeartbeatRequest: 77 | reply, err = pConn.handleHeartbeatRequest(msg) 78 | case message.MsgTypePFDManagementRequest: 79 | reply, err = pConn.handlePFDMgmtRequest(msg) 80 | case message.MsgTypeAssociationSetupRequest: 81 | reply, err = pConn.handleAssociationSetupRequest(msg) 82 | if reply != nil && err == nil && pConn.upf.enableHBTimer { 83 | go pConn.startHeartBeatMonitor() 84 | } 85 | // TODO: Cleanup sessions 86 | 87 | case message.MsgTypeAssociationReleaseRequest: 88 | reply, err = pConn.handleAssociationReleaseRequest(msg) 89 | defer pConn.Shutdown() 90 | 91 | // Session related messages 92 | case message.MsgTypeSessionEstablishmentRequest: 93 | reply, err = pConn.handleSessionEstablishmentRequest(msg) 94 | case message.MsgTypeSessionModificationRequest: 95 | reply, err = pConn.handleSessionModificationRequest(msg) 96 | case message.MsgTypeSessionDeletionRequest: 97 | reply, err = pConn.handleSessionDeletionRequest(msg) 98 | case message.MsgTypeSessionReportResponse: 99 | err = pConn.handleSessionReportResponse(msg) 100 | 101 | // Incoming response messages 102 | // TODO: Session Report Request 103 | case message.MsgTypeAssociationSetupResponse, message.MsgTypeHeartbeatResponse: 104 | pConn.handleIncomingResponse(msg) 105 | 106 | default: 107 | logger.PfcpLog.Errorf("message type: %s is not currently supported", msgType) 108 | return 109 | } 110 | 111 | nodeID := pConn.nodeID.remote 112 | // Check for errors in handling the message 113 | if err != nil { 114 | m.Finish(nodeID, "Failure") 115 | logger.PfcpLog.Errorf("error handling PFCP message type %s, from: %s, nodeID: %s, error: %v", msgType, addr, nodeID, err) 116 | } else { 117 | m.Finish(nodeID, "Success") 118 | logger.PfcpLog.Debugf("successfully processed %s, from %s, nodeID: %s", msgType, addr, nodeID) 119 | } 120 | 121 | pConn.SaveMessages(m) 122 | 123 | if reply != nil { 124 | pConn.SendPFCPMsg(reply) 125 | } 126 | } 127 | 128 | func (pConn *PFCPConn) SendPFCPMsg(msg message.Message) { 129 | addr := pConn.RemoteAddr().String() 130 | nodeID := pConn.nodeID.remote 131 | msgType := msg.MessageTypeName() 132 | 133 | m := metrics.NewMessage(msgType, "Outgoing") 134 | defer pConn.SaveMessages(m) 135 | 136 | out := make([]byte, msg.MarshalLen()) 137 | 138 | if err := msg.MarshalTo(out); err != nil { 139 | m.Finish(nodeID, "Failure") 140 | logger.PfcpLog.Errorf("failed to marshal %s for %s, error: %v", msgType, addr, err) 141 | 142 | return 143 | } 144 | 145 | if _, err := pConn.Write(out); err != nil { 146 | m.Finish(nodeID, "Failure") 147 | logger.PfcpLog.Errorf("failed to transmit %v to %s, error %v", msgType, addr, err) 148 | 149 | return 150 | } 151 | 152 | m.Finish(nodeID, "Success") 153 | logger.PfcpLog.Debugf("sent %v to %s", msgType, addr) 154 | } 155 | 156 | func (pConn *PFCPConn) sendPFCPRequestMessage(r *Request) (message.Message, bool) { 157 | pConn.pendingReqs.Store(r.msg.Sequence(), r) 158 | 159 | pConn.SendPFCPMsg(r.msg) 160 | retriesLeft := pConn.upf.maxReqRetries 161 | 162 | for { 163 | if reply, rc := r.GetResponse(pConn.shutdown, pConn.upf.respTimeout); rc { 164 | logger.PfcpLog.Debugln("request timeout, retries left:", retriesLeft) 165 | 166 | if retriesLeft > 0 { 167 | pConn.SendPFCPMsg(r.msg) 168 | retriesLeft-- 169 | } else { 170 | return nil, true 171 | } 172 | } else { 173 | return reply, false 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /pfcpiface/metrics.txt.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /pfcpiface/metrics/interface.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021-present Intel Corporation 3 | 4 | package metrics 5 | 6 | import "time" 7 | 8 | type Message struct { 9 | NodeID string 10 | MsgType string 11 | Direction string 12 | Result string 13 | 14 | StartedAt time.Time 15 | Duration float64 16 | } 17 | 18 | func NewMessage(msgType, direction string) *Message { 19 | return &Message{ 20 | MsgType: msgType, 21 | Direction: direction, 22 | 23 | StartedAt: time.Now(), 24 | } 25 | } 26 | 27 | func (m *Message) Finish(nodeID, result string) { 28 | m.NodeID = nodeID 29 | m.Result = result 30 | m.Duration = time.Since(m.StartedAt).Seconds() 31 | } 32 | 33 | type Session struct { 34 | NodeID string 35 | 36 | CreatedAt time.Time 37 | Duration float64 38 | } 39 | 40 | func NewSession(nodeID string) *Session { 41 | return &Session{ 42 | NodeID: nodeID, 43 | CreatedAt: time.Now(), 44 | } 45 | } 46 | 47 | func (s *Session) Delete() { 48 | s.Duration = time.Since(s.CreatedAt).Seconds() 49 | } 50 | 51 | type InstrumentPFCP interface { 52 | SaveMessages(m *Message) 53 | SaveSessions(s *Session) 54 | Stop() error 55 | } 56 | -------------------------------------------------------------------------------- /pfcpiface/metrics/prometheus.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021-present Intel Corporation 3 | 4 | package metrics 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | ) 11 | 12 | type Service struct { 13 | msgCount *prometheus.CounterVec 14 | msgDuration *prometheus.HistogramVec 15 | 16 | sessions *prometheus.GaugeVec 17 | sessionDuration *prometheus.HistogramVec 18 | } 19 | 20 | func NewPrometheusService() (*Service, error) { 21 | msgCount := prometheus.NewCounterVec(prometheus.CounterOpts{ 22 | Name: "pfcp_messages_total", 23 | Help: "Counter for incoming and outgoing PFCP messages", 24 | }, []string{"node_id", "message_type", "direction", "result"}) 25 | 26 | if err := prometheus.Register(msgCount); err != nil { 27 | return nil, err 28 | } 29 | 30 | msgDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ 31 | Name: "pfcp_messages_duration_seconds", 32 | Help: "The latency of the PFCP request", 33 | Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 1e1}, 34 | }, []string{"node_id", "message_type", "direction"}) 35 | 36 | if err := prometheus.Register(msgDuration); err != nil { 37 | return nil, err 38 | } 39 | 40 | sessions := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 41 | Name: "pfcp_sessions", 42 | Help: "Number of PFCP sessions currently in the UPF", 43 | }, []string{"node_id"}) 44 | 45 | if err := prometheus.Register(sessions); err != nil { 46 | return nil, err 47 | } 48 | 49 | sessionDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ 50 | Name: "pfcp_session_duration_seconds", 51 | Help: "The lifetime of PFCP session", 52 | Buckets: []float64{ 53 | 1 * time.Minute.Seconds(), 54 | 10 * time.Minute.Seconds(), 55 | 30 * time.Minute.Seconds(), 56 | 57 | 1 * time.Hour.Seconds(), 58 | 6 * time.Hour.Seconds(), 59 | 12 * time.Hour.Seconds(), 60 | 24 * time.Hour.Seconds(), 61 | 62 | 7 * 24 * time.Hour.Seconds(), 63 | 4 * 7 * 24 * time.Hour.Seconds(), 64 | }, 65 | }, []string{"node_id"}) 66 | 67 | if err := prometheus.Register(sessionDuration); err != nil { 68 | return nil, err 69 | } 70 | 71 | s := &Service{ 72 | msgCount: msgCount, 73 | msgDuration: msgDuration, 74 | 75 | sessions: sessions, 76 | sessionDuration: sessionDuration, 77 | } 78 | 79 | return s, nil 80 | } 81 | 82 | func (s *Service) SaveMessages(msg *Message) { 83 | s.msgCount.WithLabelValues(msg.NodeID, msg.MsgType, msg.Direction, msg.Result).Inc() 84 | s.msgDuration.WithLabelValues(msg.NodeID, msg.MsgType, msg.Direction).Observe(msg.Duration) 85 | } 86 | 87 | func (s *Service) SaveSessions(sess *Session) { 88 | if sess.Duration == 0 { 89 | s.sessions.WithLabelValues(sess.NodeID).Inc() 90 | return 91 | } 92 | 93 | s.sessions.WithLabelValues(sess.NodeID).Dec() 94 | s.sessionDuration.WithLabelValues(sess.NodeID).Observe(sess.Duration) 95 | } 96 | 97 | func (s *Service) Stop() error { 98 | prometheus.Unregister(s.msgCount) 99 | prometheus.Unregister(s.msgDuration) 100 | prometheus.Unregister(s.sessions) 101 | prometheus.Unregister(s.sessionDuration) 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /pfcpiface/metrics/prometheus_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package metrics 5 | 6 | import ( 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/stretchr/testify/require" 9 | 10 | "testing" 11 | ) 12 | 13 | // TODO: we currently need to reset the DefaultRegisterer between tests, as some 14 | // leave the registry in a bad state. Use custom registries to avoid global state. 15 | var backupGlobalRegistry prometheus.Registerer 16 | 17 | func saveReg() { 18 | backupGlobalRegistry = prometheus.DefaultRegisterer 19 | prometheus.DefaultRegisterer = prometheus.NewRegistry() 20 | } 21 | 22 | func restoreReg() { 23 | prometheus.DefaultRegisterer = backupGlobalRegistry 24 | } 25 | 26 | func TestNewPrometheusService(t *testing.T) { 27 | t.Run("cannot register multiple times without stop", func(t *testing.T) { 28 | saveReg() 29 | defer restoreReg() 30 | 31 | _, err := NewPrometheusService() 32 | require.NoError(t, err) 33 | 34 | _, err = NewPrometheusService() 35 | require.Error(t, err) 36 | }) 37 | 38 | t.Run("can register multiple times with stop", func(t *testing.T) { 39 | saveReg() 40 | defer restoreReg() 41 | 42 | var s *Service 43 | s, err := NewPrometheusService() 44 | require.NoError(t, err) 45 | 46 | err = s.Stop() 47 | require.NoError(t, err) 48 | 49 | _, err = NewPrometheusService() 50 | require.NoError(t, err) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /pfcpiface/node.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021 Intel Corporation 3 | // Copyright 2021 Open Networking Foundation 4 | package pfcpiface 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net" 10 | "sync" 11 | 12 | reuse "github.com/libp2p/go-reuseport" 13 | 14 | "github.com/omec-project/upf-epc/logger" 15 | "github.com/omec-project/upf-epc/pfcpiface/metrics" 16 | ) 17 | 18 | // PFCPNode represents a PFCP endpoint of the UPF. 19 | type PFCPNode struct { 20 | ctx context.Context 21 | cancel context.CancelFunc 22 | // listening socket for new "PFCP connections" 23 | net.PacketConn 24 | // done is closed to signal shutdown complete 25 | done chan struct{} 26 | // channel for PFCPConn to signal exit by sending their remote address 27 | pConnDone chan string 28 | // map of existing connections 29 | pConns sync.Map 30 | // upf 31 | upf *upf 32 | // metrics for PFCP messages and sessions 33 | metrics metrics.InstrumentPFCP 34 | } 35 | 36 | // NewPFCPNode create a new PFCPNode listening on local address. 37 | func NewPFCPNode(upf *upf) *PFCPNode { 38 | conn, err := reuse.ListenPacket("udp", 39 | upf.n4addr+":"+PFCPPort) 40 | if err != nil { 41 | logger.PfcpLog.Fatalln("listen UDP failed", err) 42 | } 43 | 44 | metrics, err := metrics.NewPrometheusService() 45 | if err != nil { 46 | logger.PfcpLog.Fatalln("prom metrics service init failed", err) 47 | } 48 | 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | 51 | return &PFCPNode{ 52 | ctx: ctx, 53 | cancel: cancel, 54 | PacketConn: conn, 55 | done: make(chan struct{}), 56 | pConnDone: make(chan string, 100), 57 | upf: upf, 58 | metrics: metrics, 59 | } 60 | } 61 | 62 | func (node *PFCPNode) tryConnectToN4Peers(lAddrStr string) { 63 | for _, peer := range node.upf.peers { 64 | conn, err := net.Dial("udp", peer+":"+PFCPPort) 65 | if err != nil { 66 | logger.PfcpLog.Warnln("failed to establish PFCP connection to peer", peer) 67 | continue 68 | } 69 | 70 | remoteAddr := conn.RemoteAddr().(*net.UDPAddr) 71 | n4DstIP := remoteAddr.IP 72 | 73 | logger.PfcpLog.Infof("Establishing PFCP Conn with CP node. SPGWC/SMF host: %s, CP node: %s", peer, n4DstIP.String()) 74 | 75 | pfcpConn := node.NewPFCPConn(lAddrStr, n4DstIP.String()+":"+PFCPPort, nil) 76 | if pfcpConn != nil { 77 | go pfcpConn.sendAssociationRequest() 78 | } 79 | } 80 | } 81 | 82 | func (node *PFCPNode) handleNewPeers() { 83 | lAddrStr := node.LocalAddr().String() 84 | logger.PfcpLog.Infoln("listening for new PFCP connections on", lAddrStr) 85 | 86 | node.tryConnectToN4Peers(lAddrStr) 87 | 88 | for { 89 | buf := make([]byte, 1024) 90 | 91 | n, rAddr, err := node.ReadFrom(buf) 92 | if err != nil { 93 | if errors.Is(err, net.ErrClosed) { 94 | return 95 | } 96 | 97 | continue 98 | } 99 | 100 | rAddrStr := rAddr.String() 101 | 102 | _, ok := node.pConns.Load(rAddrStr) 103 | if ok { 104 | logger.PfcpLog.Warnln("drop packet for existing PFCPconn received from", rAddrStr) 105 | continue 106 | } 107 | 108 | node.NewPFCPConn(lAddrStr, rAddrStr, buf[:n]) 109 | } 110 | } 111 | 112 | // Serve listens for the first packet from a new PFCP peer and creates PFCPConn. 113 | func (node *PFCPNode) Serve() { 114 | go node.handleNewPeers() 115 | 116 | shutdown := false 117 | 118 | for !shutdown { 119 | select { 120 | case fseid := <-node.upf.reportNotifyChan: 121 | // TODO: Logic to distinguish PFCPConn based on SEID 122 | node.pConns.Range(func(key, value interface{}) bool { 123 | pConn := value.(*PFCPConn) 124 | pConn.handleDigestReport(fseid) 125 | return false 126 | }) 127 | case rAddr := <-node.pConnDone: 128 | node.pConns.Delete(rAddr) 129 | logger.PfcpLog.Infoln("removed connection to", rAddr) 130 | case <-node.ctx.Done(): 131 | shutdown = true 132 | 133 | logger.PfcpLog.Infoln("shutting down PFCP node") 134 | 135 | err := node.Close() 136 | if err != nil { 137 | logger.PfcpLog.Errorln("error closing PFCPNode conn", err) 138 | } 139 | 140 | // Clear out the remaining pconn completions 141 | clearLoop: 142 | for { 143 | select { 144 | case rAddr, ok := <-node.pConnDone: 145 | { 146 | if !ok { 147 | // channel is closed, break 148 | break clearLoop 149 | } 150 | node.pConns.Delete(rAddr) 151 | logger.PfcpLog.Infoln("removed connection to", rAddr) 152 | } 153 | default: 154 | // nothing to read from channel 155 | break clearLoop 156 | } 157 | } 158 | 159 | if len(node.pConnDone) > 0 { 160 | for rAddr := range node.pConnDone { 161 | node.pConns.Delete(rAddr) 162 | logger.PfcpLog.Infoln("removed connection to", rAddr) 163 | } 164 | } 165 | 166 | close(node.pConnDone) 167 | logger.PfcpLog.Infoln("done waiting for PFCPConn completions") 168 | 169 | node.upf.Exit() 170 | } 171 | } 172 | 173 | close(node.done) 174 | } 175 | 176 | func (node *PFCPNode) Stop() { 177 | node.cancel() 178 | 179 | if err := node.metrics.Stop(); err != nil { 180 | // TODO: propagate error upwards 181 | logger.PfcpLog.Errorln(err) 182 | } 183 | } 184 | 185 | // Done waits for Shutdown() to complete 186 | func (node *PFCPNode) Done() { 187 | <-node.done 188 | logger.PfcpLog.Infoln("shutdown complete") 189 | } 190 | -------------------------------------------------------------------------------- /pfcpiface/notifier.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type downlinkDataNotifier struct { 12 | notifyChan chan<- uint64 13 | 14 | notificationInterval time.Duration 15 | 16 | // state keeps track of F-SEIDs and corresponding notification time in future 17 | state sync.Map 18 | } 19 | 20 | func NewDownlinkDataNotifier(notifyChan chan<- uint64, notificationInterval time.Duration) *downlinkDataNotifier { 21 | return &downlinkDataNotifier{ 22 | notifyChan: notifyChan, 23 | notificationInterval: notificationInterval, 24 | } 25 | } 26 | 27 | // Notify checks if DDN should be generated and sends event to notifyChan. 28 | func (n *downlinkDataNotifier) Notify(fseid uint64) { 29 | if !n.shouldNotify(fseid) { 30 | return 31 | } 32 | 33 | n.notifyChan <- fseid 34 | } 35 | 36 | // shouldNotify checks if DDN can be generated. 37 | // DDN is generated if: 38 | // 1) notification timer has expired, or 39 | // 2) notification for unknown F-SEID is received 40 | func (n *downlinkDataNotifier) shouldNotify(fseid uint64) bool { 41 | entry, ok := n.state.Load(fseid) 42 | if !ok { 43 | n.state.Store(fseid, time.Now()) 44 | // TODO: add goroutine that will remove stale entries 45 | return true 46 | } 47 | 48 | lastTimestamp := entry.(time.Time) 49 | 50 | if time.Since(lastTimestamp) >= n.notificationInterval { 51 | n.state.Store(fseid, time.Now()) 52 | return true 53 | } 54 | 55 | return false 56 | } 57 | -------------------------------------------------------------------------------- /pfcpiface/notifier_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_downlinkDataNotifier_Notify(t *testing.T) { 14 | ch := make(chan<- uint64, 1024) 15 | n := NewDownlinkDataNotifier(ch, 5*time.Second) 16 | 17 | testFSEID := uint64(0x1) 18 | 19 | n.Notify(testFSEID) 20 | require.Len(t, n.notifyChan, 1) 21 | n.Notify(testFSEID) 22 | // we haven't picked any event from channel, so length should be the same. 23 | require.Len(t, n.notifyChan, 1) 24 | } 25 | 26 | func Test_downlinkDataNotifier_shouldNotify(t *testing.T) { 27 | t.Run("single F-SEID check rate limiting", func(t *testing.T) { 28 | ch := make(chan<- uint64, 1024) 29 | n := NewDownlinkDataNotifier(ch, 5*time.Second) 30 | testFSEID := uint64(0x1) 31 | 32 | got := n.shouldNotify(testFSEID) 33 | require.True(t, got) 34 | <-time.After(3 * time.Second) 35 | 36 | got = n.shouldNotify(testFSEID) 37 | require.False(t, got) 38 | <-time.After(1 * time.Second) 39 | 40 | got = n.shouldNotify(testFSEID) 41 | require.False(t, got) 42 | <-time.After(2 * time.Second) 43 | 44 | // after ~6 seconds 45 | got = n.shouldNotify(testFSEID) 46 | require.True(t, got) 47 | 48 | got = n.shouldNotify(testFSEID) 49 | require.False(t, got) 50 | <-time.After(1 * time.Second) 51 | }) 52 | 53 | t.Run("multiple F-SEIDs check rate limiting", func(t *testing.T) { 54 | ch := make(chan<- uint64, 1024) 55 | n := NewDownlinkDataNotifier(ch, 5*time.Second) 56 | 57 | // generate 100k unique F-SEIDs 58 | testFSEIDs := make([]uint64, 0) 59 | for i := 1; i < 100000; i++ { 60 | testFSEIDs = append(testFSEIDs, uint64(i)) 61 | } 62 | 63 | for _, fseid := range testFSEIDs { 64 | got := n.shouldNotify(fseid) 65 | require.True(t, got) 66 | } 67 | 68 | <-time.After(3 * time.Second) 69 | 70 | for _, fseid := range testFSEIDs { 71 | got := n.shouldNotify(fseid) 72 | require.False(t, got) 73 | } 74 | 75 | <-time.After(3 * time.Second) 76 | 77 | for _, fseid := range testFSEIDs { 78 | got := n.shouldNotify(fseid) 79 | require.True(t, got) 80 | } 81 | 82 | <-time.After(1 * time.Second) 83 | 84 | for _, fseid := range testFSEIDs { 85 | got := n.shouldNotify(fseid) 86 | require.False(t, got) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /pfcpiface/parse_far.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/omec-project/upf-epc/logger" 10 | "github.com/wmnsk/go-pfcp/ie" 11 | ) 12 | 13 | type operation int 14 | 15 | const ( 16 | FwdIEOuterHeaderCreation Bits = 1 << iota 17 | FwdIEDestinationIntf 18 | FwdIEPfcpSMReqFlags 19 | ) 20 | 21 | const ( 22 | ActionForward = 0x2 23 | ActionDrop = 0x1 24 | ActionBuffer = 0x4 25 | ActionNotify = 0x8 26 | ) 27 | 28 | const ( 29 | create operation = iota 30 | update 31 | ) 32 | 33 | type far struct { 34 | farID uint32 35 | fseID uint64 36 | fseidIP uint32 37 | 38 | dstIntf uint8 39 | sendEndMarker bool 40 | applyAction uint8 41 | tunnelType uint8 42 | tunnelIP4Src uint32 43 | tunnelIP4Dst uint32 44 | tunnelTEID uint32 45 | tunnelPort uint16 46 | } 47 | 48 | func (f far) String() string { 49 | return fmt.Sprintf("FAR(id=%v, F-SEID=%v, F-SEID IPv4=%v, dstInterface=%v, tunnelType=%v, "+ 50 | "tunnelIPv4Src=%v, tunnelIPv4Dst=%v, tunnelTEID=%v, tunnelSrcPort=%v, "+ 51 | "sendEndMarker=%v, drops=%v, forwards=%v, buffers=%v)", f.farID, f.fseID, int2ip(f.fseidIP), f.dstIntf, 52 | f.tunnelType, int2ip(f.tunnelIP4Src), int2ip(f.tunnelIP4Dst), f.tunnelTEID, f.tunnelPort, f.sendEndMarker, 53 | f.Drops(), f.Forwards(), f.Buffers()) 54 | } 55 | 56 | func (f *far) Drops() bool { 57 | return f.applyAction&ActionDrop != 0 58 | } 59 | 60 | func (f *far) Buffers() bool { 61 | return f.applyAction&ActionBuffer != 0 62 | } 63 | 64 | func (f *far) Forwards() bool { 65 | return f.applyAction&ActionForward != 0 66 | } 67 | 68 | func (f *far) parseFAR(farIE *ie.IE, fseid uint64, upf *upf, op operation) error { 69 | f.fseID = (fseid) 70 | 71 | farID, err := farIE.FARID() 72 | if err != nil { 73 | return err 74 | } 75 | 76 | f.farID = farID 77 | 78 | action, err := farIE.ApplyAction() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if action[0] == 0 { 84 | return ErrInvalidArgument("FAR Action", action) 85 | } 86 | 87 | f.applyAction = action[0] 88 | 89 | var fwdIEs []*ie.IE 90 | 91 | switch op { 92 | case create: 93 | if (f.applyAction & ActionForward) != 0 { 94 | fwdIEs, err = farIE.ForwardingParameters() 95 | } 96 | case update: 97 | fwdIEs, err = farIE.UpdateForwardingParameters() 98 | default: 99 | return ErrInvalidOperation(op) 100 | } 101 | 102 | if err != nil { 103 | return err 104 | } 105 | 106 | f.sendEndMarker = false 107 | 108 | var fields Bits 109 | var ohcFields *ie.OuterHeaderCreationFields 110 | 111 | for _, fwdIE := range fwdIEs { 112 | switch fwdIE.Type { 113 | case ie.OuterHeaderCreation: 114 | fields = Set(fields, FwdIEOuterHeaderCreation) 115 | 116 | ohcFields, err = fwdIE.OuterHeaderCreation() 117 | if err != nil { 118 | logger.PfcpLog.Errorln("unable to parse OuterHeaderCreationFields") 119 | continue 120 | } 121 | 122 | f.tunnelTEID = ohcFields.TEID 123 | f.tunnelIP4Dst = ip2int(ohcFields.IPv4Address) 124 | f.tunnelType = uint8(1) // FIXME: what does it mean? 125 | f.tunnelPort = tunnelGTPUPort 126 | case ie.DestinationInterface: 127 | fields = Set(fields, FwdIEDestinationIntf) 128 | 129 | f.dstIntf, err = fwdIE.DestinationInterface() 130 | if err != nil { 131 | logger.PfcpLog.Errorln("unable to parse DestinationInterface field") 132 | continue 133 | } 134 | 135 | switch f.dstIntf { 136 | case ie.DstInterfaceAccess: 137 | f.tunnelIP4Src = ip2int(upf.accessIP) 138 | case ie.DstInterfaceCore: 139 | f.tunnelIP4Src = ip2int(upf.coreIP) 140 | } 141 | case ie.PFCPSMReqFlags: 142 | fields = Set(fields, FwdIEPfcpSMReqFlags) 143 | 144 | smReqFlags, err := fwdIE.PFCPSMReqFlags() 145 | if err != nil { 146 | logger.PfcpLog.Errorln("unable to parse PFCPSMReqFlags") 147 | continue 148 | } 149 | 150 | if has2ndBit(smReqFlags) { 151 | f.sendEndMarker = true 152 | } 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /pfcpiface/parse_far_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "net" 8 | "testing" 9 | 10 | pfcpsimLib "github.com/omec-project/pfcpsim/pkg/pfcpsim/session" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "github.com/wmnsk/go-pfcp/ie" 14 | ) 15 | 16 | type farTestCase struct { 17 | input *ie.IE 18 | op operation 19 | expected *far 20 | description string 21 | } 22 | 23 | const ( 24 | defaultGTPProtocolPort = 2152 25 | ) 26 | 27 | func TestParseFAR(t *testing.T) { 28 | createOp, updateOp := create, update 29 | 30 | var FSEID uint64 = 100 31 | 32 | coreIP := net.ParseIP("10.0.10.1") 33 | UEAddressForDownlink := net.ParseIP("10.0.1.1") 34 | 35 | for _, scenario := range []farTestCase{ 36 | { 37 | op: createOp, 38 | input: pfcpsimLib.NewFARBuilder(). 39 | WithID(999). 40 | WithMethod(pfcpsimLib.Create). 41 | WithAction(ActionDrop). 42 | WithDstInterface(core). 43 | BuildFAR(), 44 | expected: &far{ 45 | farID: 999, 46 | applyAction: ActionDrop, 47 | fseID: FSEID, 48 | }, 49 | description: "Valid Uplink FAR input with create operation", 50 | }, 51 | { 52 | op: updateOp, 53 | input: pfcpsimLib.NewFARBuilder(). 54 | WithID(1). 55 | WithAction(ActionForward). 56 | WithMethod(pfcpsimLib.Update). 57 | WithDstInterface(access). 58 | WithDownlinkIP(UEAddressForDownlink.String()). 59 | WithTEID(100). 60 | BuildFAR(), 61 | expected: &far{ 62 | farID: 1, 63 | fseID: FSEID, 64 | applyAction: ActionForward, 65 | dstIntf: access, 66 | tunnelTEID: 100, 67 | tunnelType: access, 68 | tunnelIP4Src: ip2int(coreIP), 69 | tunnelIP4Dst: ip2int(UEAddressForDownlink), 70 | tunnelPort: uint16(defaultGTPProtocolPort), 71 | }, 72 | description: "Valid Downlink FAR input with update operation", 73 | }, 74 | } { 75 | t.Run(scenario.description, func(t *testing.T) { 76 | mockFar := &far{} 77 | mockUpf := &upf{ 78 | accessIP: net.ParseIP("192.168.0.1"), 79 | coreIP: coreIP, 80 | } 81 | 82 | err := mockFar.parseFAR(scenario.input, FSEID, mockUpf, scenario.op) 83 | require.NoError(t, err) 84 | 85 | assert.Equal(t, scenario.expected, mockFar) 86 | }) 87 | } 88 | } 89 | 90 | func TestParseFARShouldError(t *testing.T) { 91 | createOp, updateOp := create, update 92 | 93 | var FSEID uint64 = 101 94 | 95 | for _, scenario := range []farTestCase{ 96 | { 97 | op: createOp, 98 | input: ie.NewCreateFAR( 99 | ie.NewFARID(1), 100 | ie.NewApplyAction(0), 101 | ie.NewForwardingParameters( 102 | ie.NewDestinationInterface(ie.DstInterfaceCore), 103 | ), 104 | ), 105 | expected: &far{ 106 | farID: 1, 107 | fseID: FSEID, 108 | }, 109 | description: "Uplink FAR with invalid action", 110 | }, 111 | { 112 | op: updateOp, 113 | input: ie.NewUpdateFAR( 114 | ie.NewFARID(1), 115 | ie.NewApplyAction(0), 116 | ie.NewUpdateForwardingParameters( 117 | ie.NewDestinationInterface(ie.DstInterfaceAccess), 118 | ie.NewOuterHeaderCreation(0x100, 100, "10.0.0.1", "", 0, 0, 0), 119 | ), 120 | ), 121 | expected: &far{ 122 | farID: 1, 123 | fseID: FSEID, 124 | }, 125 | description: "Downlink FAR with invalid action", 126 | }, 127 | { 128 | op: createOp, 129 | input: ie.NewCreateFAR( 130 | ie.NewApplyAction(ActionDrop), 131 | ie.NewUpdateForwardingParameters( 132 | ie.NewDestinationInterface(ie.DstInterfaceAccess), 133 | ie.NewOuterHeaderCreation(0x100, 100, "10.0.0.1", "", 0, 0, 0), 134 | ), 135 | ), 136 | expected: &far{ 137 | fseID: FSEID, 138 | }, 139 | description: "Malformed Downlink FAR with missing FARID", 140 | }, 141 | } { 142 | t.Run(scenario.description, func(t *testing.T) { 143 | mockFar := &far{} 144 | mockUpf := &upf{ 145 | accessIP: net.ParseIP("192.168.0.1"), 146 | coreIP: net.ParseIP("10.0.0.1"), 147 | } 148 | 149 | err := mockFar.parseFAR(scenario.input, 101, mockUpf, scenario.op) 150 | require.Error(t, err) 151 | 152 | assert.Equal(t, scenario.expected, mockFar) 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pfcpiface/parse_qer.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/omec-project/upf-epc/logger" 10 | "github.com/wmnsk/go-pfcp/ie" 11 | ) 12 | 13 | var qosLevelName = map[QosLevel]string{ 14 | ApplicationQos: "application", 15 | SessionQos: "session", 16 | } 17 | 18 | type qer struct { 19 | qerID uint32 20 | qosLevel QosLevel 21 | qfi uint8 22 | ulStatus uint8 23 | dlStatus uint8 24 | ulMbr uint64 // in kilobits/sec 25 | dlMbr uint64 // in kilobits/sec 26 | ulGbr uint64 // in kilobits/sec 27 | dlGbr uint64 // in kilobits/sec 28 | fseID uint64 29 | fseidIP uint32 30 | } 31 | 32 | func (q qer) String() string { 33 | qosLevel, ok := qosLevelName[q.qosLevel] 34 | if !ok { 35 | qosLevel = "invalid" 36 | } 37 | 38 | return fmt.Sprintf("QER(id=%v, F-SEID=%v, F-SEID IP=%v, QFI=%v, "+ 39 | "uplinkMBR=%v, downlinkMBR=%v, uplinkGBR=%v, downlinkGBR=%v, type=%s, "+ 40 | "uplinkStatus=%v, downlinkStatus=%v)", 41 | q.qerID, q.fseID, q.fseidIP, q.qfi, q.ulMbr, q.dlMbr, q.ulGbr, q.dlGbr, 42 | qosLevel, q.ulStatus, q.dlStatus) 43 | } 44 | 45 | func (q *qer) parseQER(ie1 *ie.IE, seid uint64) error { 46 | qerID, err := ie1.QERID() 47 | if err != nil { 48 | logger.PfcpLog.Errorln("could not read QER ID") 49 | return err 50 | } 51 | 52 | qfi, err := ie1.QFI() 53 | if err != nil { 54 | logger.PfcpLog.Errorln("could not read QFI") 55 | } 56 | 57 | gsUL, err := ie1.GateStatusUL() 58 | if err != nil { 59 | logger.PfcpLog.Errorln("could not read Gate status uplink") 60 | } 61 | 62 | gsDL, err := ie1.GateStatusDL() 63 | if err != nil { 64 | logger.PfcpLog.Errorln("could not read Gate status downlink") 65 | } 66 | 67 | mbrUL, err := ie1.MBRUL() 68 | if err != nil { 69 | logger.PfcpLog.Errorln("could not read MBRUL") 70 | } 71 | 72 | mbrDL, err := ie1.MBRDL() 73 | if err != nil { 74 | logger.PfcpLog.Errorln("could not read MBRDL") 75 | } 76 | 77 | gbrUL, err := ie1.GBRUL() 78 | if err != nil { 79 | logger.PfcpLog.Warnln("could not read GBRUL. It might be because of non-GBR flow") 80 | } 81 | 82 | gbrDL, err := ie1.GBRDL() 83 | if err != nil { 84 | logger.PfcpLog.Warnln("could not read GBRDL. It might be because of non-GBR flow") 85 | } 86 | 87 | q.qerID = qerID 88 | q.qfi = qfi 89 | q.ulStatus = gsUL 90 | q.dlStatus = gsDL 91 | q.ulMbr = mbrUL 92 | q.dlMbr = mbrDL 93 | q.ulGbr = gbrUL 94 | q.dlGbr = gbrDL 95 | q.fseID = seid 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pfcpiface/parse_qer_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "testing" 8 | 9 | pfcpsimLib "github.com/omec-project/pfcpsim/pkg/pfcpsim/session" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "github.com/wmnsk/go-pfcp/ie" 14 | ) 15 | 16 | type qerTestCase struct { 17 | input *ie.IE 18 | expected *qer 19 | description string 20 | } 21 | 22 | func TestParseQER(t *testing.T) { 23 | FSEID := uint64(100) 24 | 25 | for _, scenario := range []qerTestCase{ 26 | { 27 | input: pfcpsimLib.NewQERBuilder(). 28 | WithID(999). 29 | WithMethod(pfcpsimLib.IEMethod(create)). 30 | WithQFI(0x09).Build(), 31 | expected: &qer{ 32 | qerID: 999, 33 | qfi: 0x09, 34 | fseID: FSEID, 35 | }, 36 | description: "Valid Create QER input", 37 | }, 38 | { 39 | input: pfcpsimLib.NewQERBuilder(). 40 | WithID(999). 41 | WithMethod(pfcpsimLib.IEMethod(update)). 42 | WithQFI(0x09).Build(), 43 | expected: &qer{ 44 | qerID: 999, 45 | qfi: 0x09, 46 | fseID: FSEID, 47 | }, 48 | description: "Valid Update QER input", 49 | }, 50 | } { 51 | t.Run(scenario.description, func(t *testing.T) { 52 | mockQER := &qer{} 53 | 54 | err := mockQER.parseQER(scenario.input, FSEID) 55 | require.NoError(t, err) 56 | 57 | assert.Equal(t, scenario.expected, mockQER) 58 | }) 59 | } 60 | } 61 | 62 | func TestParseQERShouldError(t *testing.T) { 63 | FSEID := uint64(100) 64 | 65 | for _, scenario := range []qerTestCase{ 66 | { 67 | input: ie.NewCreateQER( 68 | ie.NewQFI(64), 69 | ie.NewGateStatus(0, 0), 70 | ie.NewMBR(0, 1), 71 | ie.NewGBR(2, 3), 72 | ), 73 | expected: &qer{}, 74 | description: "Invalid QER input: no QER ID provided", 75 | }, 76 | } { 77 | t.Run(scenario.description, func(t *testing.T) { 78 | mockQER := &qer{} 79 | 80 | err := mockQER.parseQER(scenario.input, FSEID) 81 | require.Error(t, err) 82 | 83 | assert.Equal(t, scenario.expected, mockQER) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pfcpiface/parse_sdf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | // Copyright 2022 Open Networking Foundation 4 | 5 | package pfcpiface 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/omec-project/upf-epc/logger" 15 | ) 16 | 17 | const ( 18 | reservedProto = uint8(0xff) 19 | Ipv4WildcardNetString = "0.0.0.0/0" 20 | ) 21 | 22 | var errBadFilterDesc = errors.New("unsupported Filter Description format") 23 | 24 | type endpoint struct { 25 | IPNet *net.IPNet 26 | ports portRange 27 | } 28 | 29 | func (ep *endpoint) parseNet(ipnet string) error { 30 | ipNetFields := strings.Split(ipnet, "/") 31 | 32 | switch len(ipNetFields) { 33 | case 1: 34 | ipnet = ipNetFields[0] + "/32" 35 | case 2: 36 | default: 37 | return ErrInvalidArgument("network string", len(ipNetFields)) 38 | } 39 | 40 | var err error 41 | 42 | _, ep.IPNet, err = net.ParseCIDR(ipnet) 43 | if err != nil { 44 | return ErrOperationFailedWithReason("ParseCIDR", err.Error()) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (ep *endpoint) parsePort(port string) error { 51 | ports := strings.Split(port, "-") 52 | if len(ports) == 0 || len(ports) > 2 { 53 | return ErrInvalidArgument("port string", port) 54 | } 55 | // Pretend this is a port range with one element. 56 | if len(ports) == 1 { 57 | ports = append(ports, ports[0]) 58 | } 59 | 60 | low, err := strconv.ParseUint(ports[0], 10, 16) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | high, err := strconv.ParseUint(ports[1], 10, 16) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if low > high { 71 | return ErrInvalidArgumentWithReason("port", port, "invalid port range") 72 | } 73 | 74 | ep.ports = newRangeMatchPortRange(uint16(low), uint16(high)) 75 | 76 | return nil 77 | } 78 | 79 | type ipFilterRule struct { 80 | action, direction string 81 | proto uint8 82 | src, dst endpoint 83 | } 84 | 85 | func newIpFilterRule() *ipFilterRule { 86 | return &ipFilterRule{ 87 | src: endpoint{ports: newWildcardPortRange()}, 88 | dst: endpoint{ports: newWildcardPortRange()}, 89 | } 90 | } 91 | 92 | func (ipf *ipFilterRule) String() string { 93 | return fmt.Sprintf("FlowDescription{action=%v, direction=%v, proto=%v, "+ 94 | "srcIP=%v, srcPort=%v, dstIP=%v, dstPort=%v}", 95 | ipf.action, ipf.direction, ipf.proto, ipf.src.IPNet, ipf.src.ports, ipf.dst.IPNet, ipf.dst.ports) 96 | } 97 | 98 | func parseFlowDesc(flowDesc, ueIP string) (*ipFilterRule, error) { 99 | parseLog := logger.PfcpLog.With("flow-description", flowDesc, "ue-address", ueIP) 100 | parseLog.Debugln("parsing flow description") 101 | 102 | ipf := newIpFilterRule() 103 | 104 | fields := strings.Fields(flowDesc) 105 | if len(fields) < 3 { 106 | return nil, errBadFilterDesc 107 | } 108 | 109 | if err := parseAction(fields[0]); err != nil { 110 | return nil, err 111 | } 112 | 113 | ipf.action = fields[0] 114 | 115 | if err := parseDirection(fields[1]); err != nil { 116 | return nil, err 117 | } 118 | 119 | ipf.direction = fields[1] 120 | ipf.proto, _ = parseL4Proto(fields[2]) 121 | 122 | // bring to common intermediate representation 123 | xform := func(i int) { 124 | switch fields[i] { 125 | case "any": 126 | fields[i] = Ipv4WildcardNetString 127 | case "assigned": 128 | if ueIP == "0.0.0.0" { 129 | fields[i] = Ipv4WildcardNetString 130 | } else if ueIP != "" && ueIP != "" { 131 | fields[i] = ueIP 132 | } else { 133 | fields[i] = Ipv4WildcardNetString 134 | } 135 | } 136 | } 137 | 138 | for i := 3; i < len(fields); i++ { 139 | switch fields[i] { 140 | case "from": 141 | i++ 142 | xform(i) 143 | 144 | err := ipf.src.parseNet(fields[i]) 145 | if err != nil { 146 | parseLog.Errorln(err) 147 | return nil, err 148 | } 149 | 150 | if fields[i+1] != "to" { 151 | i++ 152 | 153 | err = ipf.src.parsePort(fields[i]) 154 | if err != nil { 155 | parseLog.Errorln("src port parse failed", err) 156 | return nil, err 157 | } 158 | } 159 | case "to": 160 | i++ 161 | xform(i) 162 | 163 | err := ipf.dst.parseNet(fields[i]) 164 | if err != nil { 165 | parseLog.Errorln(err) 166 | return nil, err 167 | } 168 | 169 | if i < len(fields)-1 { 170 | i++ 171 | 172 | err = ipf.dst.parsePort(fields[i]) 173 | if err != nil { 174 | parseLog.Errorln("dst port parse failed", err) 175 | return nil, err 176 | } 177 | } 178 | } 179 | } 180 | 181 | parseLog = parseLog.With("ip-filter", ipf) 182 | parseLog.Debugln("flow description parsed successfully") 183 | 184 | return ipf, nil 185 | } 186 | 187 | func parseAction(action string) error { 188 | switch action { 189 | case "permit": 190 | case "deny": 191 | default: 192 | return errBadFilterDesc 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func parseDirection(dir string) error { 199 | switch dir { 200 | case "in": 201 | case "out": 202 | default: 203 | return errBadFilterDesc 204 | } 205 | 206 | return nil 207 | } 208 | 209 | func parseL4Proto(proto string) (uint8, error) { 210 | p, err := strconv.ParseUint(proto, 10, 8) 211 | if err == nil { 212 | return uint8(p), nil 213 | } 214 | 215 | switch proto { 216 | case "udp": 217 | return 17, nil 218 | case "tcp": 219 | return 6, nil 220 | default: 221 | return reservedProto, errBadFilterDesc 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /pfcpiface/pfcpiface.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "flag" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/omec-project/upf-epc/logger" 18 | ) 19 | 20 | var ( 21 | simulate = simModeDisable 22 | ) 23 | 24 | func init() { 25 | flag.Var(&simulate, "simulate", "create|delete|create_continue simulated sessions") 26 | } 27 | 28 | type PFCPIface struct { 29 | conf Conf 30 | 31 | node *PFCPNode 32 | fp datapath 33 | upf *upf 34 | 35 | httpSrv *http.Server 36 | httpEndpoint string 37 | 38 | uc *upfCollector 39 | nc *PfcpNodeCollector 40 | 41 | mu sync.Mutex 42 | } 43 | 44 | func NewPFCPIface(conf Conf) *PFCPIface { 45 | pfcpIface := &PFCPIface{ 46 | conf: conf, 47 | } 48 | 49 | if conf.EnableP4rt { 50 | pfcpIface.fp = &UP4{} 51 | } else { 52 | pfcpIface.fp = &bess{} 53 | } 54 | 55 | httpPort := "8080" 56 | if conf.CPIface.HTTPPort != "" { 57 | httpPort = conf.CPIface.HTTPPort 58 | } 59 | 60 | pfcpIface.httpEndpoint = ":" + httpPort 61 | 62 | pfcpIface.upf = NewUPF(&conf, pfcpIface.fp) 63 | 64 | return pfcpIface 65 | } 66 | 67 | func (p *PFCPIface) mustInit() { 68 | p.mu.Lock() 69 | defer p.mu.Unlock() 70 | 71 | p.node = NewPFCPNode(p.upf) 72 | httpMux := http.NewServeMux() 73 | 74 | setupConfigHandler(httpMux, p.upf) 75 | 76 | var err error 77 | 78 | p.uc, p.nc, err = setupProm(httpMux, p.upf, p.node) 79 | 80 | if err != nil { 81 | logger.PfcpLog.Fatalln("setupProm failed", err) 82 | } 83 | 84 | // Note: due to error with golangci-lint ("Error: G112: Potential Slowloris Attack 85 | // because ReadHeaderTimeout is not configured in the http.Server (gosec)"), 86 | // the ReadHeaderTimeout is set to the same value as in nginx (client_header_timeout) 87 | p.httpSrv = &http.Server{Addr: p.httpEndpoint, Handler: httpMux, ReadHeaderTimeout: 60 * time.Second} 88 | } 89 | 90 | func (p *PFCPIface) Run() { 91 | if simulate.enable() { 92 | p.upf.sim(simulate, &p.conf.SimInfo) 93 | 94 | if !simulate.keepGoing() { 95 | return 96 | } 97 | } 98 | 99 | p.mustInit() 100 | 101 | go func() { 102 | if err := p.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 103 | logger.PfcpLog.Fatalln("http server failed", err) 104 | } 105 | 106 | logger.PfcpLog.Infoln("http server closed") 107 | }() 108 | 109 | sig := make(chan os.Signal, 1) 110 | signal.Notify(sig, os.Interrupt) 111 | signal.Notify(sig, syscall.SIGTERM) 112 | 113 | go func() { 114 | oscall := <-sig 115 | logger.PfcpLog.Infof("system call received: %+v", oscall) 116 | p.Stop() 117 | }() 118 | 119 | // blocking 120 | p.node.Serve() 121 | } 122 | 123 | // Stop sends cancellation signal to main Go routine and waits for shutdown to complete. 124 | func (p *PFCPIface) Stop() { 125 | p.mu.Lock() 126 | defer p.mu.Unlock() 127 | 128 | ctxHttpShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) 129 | defer func() { 130 | cancel() 131 | }() 132 | 133 | if err := p.httpSrv.Shutdown(ctxHttpShutdown); err != nil { 134 | logger.PfcpLog.Errorln("failed to shutdown http:", err) 135 | } 136 | 137 | p.node.Stop() 138 | 139 | // Wait for PFCP node shutdown 140 | p.node.Done() 141 | } 142 | -------------------------------------------------------------------------------- /pfcpiface/pfd.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2021 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | // PFD holds the switch level application IDs. 7 | type appPFD struct { 8 | appID string 9 | flowDescs []string 10 | } 11 | 12 | // ResetAppPFDs resets the map of application PFDs. 13 | func (pConn *PFCPConn) ResetAppPFDs() { 14 | pConn.appPFDs = make(map[string]appPFD) 15 | } 16 | 17 | // NewAppPFD stores app PFD in session mgr. 18 | func (pConn *PFCPConn) NewAppPFD(appID string) { 19 | pConn.appPFDs[appID] = appPFD{ 20 | appID: appID, 21 | flowDescs: make([]string, 0, MaxItems), 22 | } 23 | } 24 | 25 | // RemoveAppPFD removes appPFD using appID. 26 | func (pConn *PFCPConn) RemoveAppPFD(appID string) { 27 | delete(pConn.appPFDs, appID) 28 | } 29 | -------------------------------------------------------------------------------- /pfcpiface/session_far.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/google/gopacket" 10 | "github.com/google/gopacket/layers" 11 | "github.com/omec-project/upf-epc/logger" 12 | ) 13 | 14 | // CreateFAR appends far to existing list of FARs in the session. 15 | func (s *PFCPSession) CreateFAR(f far) { 16 | s.fars = append(s.fars, f) 17 | } 18 | 19 | func addEndMarker(farItem far, endMarkerList *[][]byte) { 20 | // This time lets fill out some information 21 | logger.PfcpLog.Infoln("adding end marker for farID:", farItem.farID) 22 | 23 | options := gopacket.SerializeOptions{ 24 | ComputeChecksums: true, 25 | FixLengths: true, 26 | } 27 | buffer := gopacket.NewSerializeBuffer() 28 | ipLayer := &layers.IPv4{ 29 | Version: 4, 30 | TTL: 64, 31 | SrcIP: int2ip(farItem.tunnelIP4Src), 32 | DstIP: int2ip(farItem.tunnelIP4Dst), 33 | Protocol: layers.IPProtocolUDP, 34 | } 35 | ethernetLayer := &layers.Ethernet{ 36 | SrcMAC: net.HardwareAddr{0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA}, 37 | DstMAC: net.HardwareAddr{0xBD, 0xBD, 0xBD, 0xBD, 0xBD, 0xBD}, 38 | EthernetType: layers.EthernetTypeIPv4, 39 | } 40 | udpLayer := &layers.UDP{ 41 | SrcPort: layers.UDPPort(2152), 42 | DstPort: layers.UDPPort(2152), 43 | } 44 | 45 | err := udpLayer.SetNetworkLayerForChecksum(ipLayer) 46 | if err != nil { 47 | logger.PfcpLog.Errorln("set checksum for UDP layer in endmarker failed") 48 | return 49 | } 50 | 51 | gtpLayer := &layers.GTPv1U{ 52 | Version: 1, 53 | MessageType: 254, 54 | ProtocolType: farItem.tunnelType, 55 | TEID: farItem.tunnelTEID, 56 | } 57 | // And create the packet with the layers 58 | err = gopacket.SerializeLayers(buffer, options, 59 | ethernetLayer, 60 | ipLayer, 61 | udpLayer, 62 | gtpLayer, 63 | ) 64 | 65 | if err == nil { 66 | outgoingPacket := buffer.Bytes() 67 | *endMarkerList = append(*endMarkerList, outgoingPacket) 68 | } else { 69 | logger.PfcpLog.Errorln("go packet serialize failed:", err) 70 | } 71 | } 72 | 73 | // UpdateFAR updates existing far in the session. 74 | func (s *PFCPSession) UpdateFAR(f *far, endMarkerList *[][]byte) error { 75 | for idx, v := range s.fars { 76 | if v.farID == f.farID { 77 | if f.sendEndMarker { 78 | addEndMarker(v, endMarkerList) 79 | } 80 | 81 | s.fars[idx] = *f 82 | 83 | return nil 84 | } 85 | } 86 | 87 | return ErrNotFound("FAR") 88 | } 89 | 90 | // RemoveFAR removes far from existing list of FARs in the session. 91 | func (s *PFCPSession) RemoveFAR(id uint32) (*far, error) { 92 | for idx, v := range s.fars { 93 | if v.farID == id { 94 | s.fars = append(s.fars[:idx], s.fars[idx+1:]...) 95 | return &v, nil 96 | } 97 | } 98 | 99 | return nil, ErrNotFound("FAR") 100 | } 101 | -------------------------------------------------------------------------------- /pfcpiface/session_pdr.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "github.com/omec-project/upf-epc/logger" 8 | "github.com/wmnsk/go-pfcp/ie" 9 | "github.com/wmnsk/go-pfcp/message" 10 | ) 11 | 12 | // Release allocated IPs. 13 | func releaseAllocatedIPs(ippool *IPPool, session *PFCPSession) error { 14 | logger.PfcpLog.Infoln("release allocated IP") 15 | 16 | // Check if we allocated an UE IP for this session and delete it. 17 | for _, pdr := range session.pdrs { 18 | if (pdr.allocIPFlag) && (pdr.srcIface == core) { 19 | ueIP := int2ip(pdr.ueAddress) 20 | logger.PfcpLog.Debugf("Releasing IP %s of session %d", ueIP.String(), session.localSEID) 21 | return ippool.DeallocIP(session.localSEID) 22 | } 23 | } 24 | return nil 25 | } 26 | 27 | func addPdrInfo(msg *message.SessionEstablishmentResponse, pdrs []pdr) { 28 | logger.PfcpLog.Infoln("add PDRs with UPF alloc IPs to Establishment response") 29 | logger.PfcpLog.Infoln("PDRs:", pdrs) 30 | for _, pdr := range pdrs { 31 | logger.PfcpLog.Infoln("pdrID:", pdr.pdrID) 32 | if pdr.UPAllocateFteid { 33 | logger.PfcpLog.Infoln("adding PDR with tunnel TEID:", pdr.tunnelTEID) 34 | msg.CreatedPDR = append(msg.CreatedPDR, 35 | ie.NewCreatedPDR( 36 | ie.NewPDRID(uint16(pdr.pdrID)), 37 | ie.NewFTEID(0x01, pdr.tunnelTEID, int2ip(pdr.tunnelIP4Dst), nil, 0), 38 | )) 39 | } 40 | if (pdr.allocIPFlag) && (pdr.srcIface == core) { 41 | logger.PfcpLog.Debugln("pdrID:", pdr.pdrID) 42 | var flags uint8 = 0x02 43 | ueIP := int2ip(pdr.ueAddress) 44 | logger.PfcpLog.Debugln("ueIP:", ueIP.String()) 45 | msg.CreatedPDR = append(msg.CreatedPDR, 46 | ie.NewCreatedPDR( 47 | ie.NewPDRID(uint16(pdr.pdrID)), 48 | ie.NewUEIPAddress(flags, ueIP.String(), "", 0, 0), 49 | )) 50 | } 51 | } 52 | } 53 | 54 | // CreatePDR appends pdr to existing list of PDRs in the session. 55 | func (s *PFCPSession) CreatePDR(p pdr) { 56 | s.pdrs = append(s.pdrs, p) 57 | } 58 | 59 | // UpdatePDR updates existing pdr in the session. 60 | func (s *PFCPSession) UpdatePDR(p pdr) error { 61 | for idx, v := range s.pdrs { 62 | if v.pdrID == p.pdrID { 63 | s.pdrs[idx] = p 64 | return nil 65 | } 66 | } 67 | 68 | return ErrNotFound("PDR") 69 | } 70 | 71 | // RemovePDR removes pdr from existing list of PDRs in the session. 72 | func (s *PFCPSession) RemovePDR(id uint32) (*pdr, error) { 73 | for idx, v := range s.pdrs { 74 | if v.pdrID == id { 75 | s.pdrs = append(s.pdrs[:idx], s.pdrs[idx+1:]...) 76 | return &v, nil 77 | } 78 | } 79 | 80 | return nil, ErrNotFound("PDR") 81 | } 82 | -------------------------------------------------------------------------------- /pfcpiface/session_qer.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "github.com/omec-project/upf-epc/logger" 8 | ) 9 | 10 | type QosLevel uint8 11 | 12 | const ( 13 | ApplicationQos QosLevel = 0 14 | SessionQos QosLevel = 1 15 | ) 16 | 17 | // CreateQER appends qer to existing list of QERs in the session. 18 | func (s *PFCPSession) CreateQER(q qer) { 19 | s.qers = append(s.qers, q) 20 | } 21 | 22 | // UpdateQER updates existing qer in the session. 23 | func (s *PFCPSession) UpdateQER(q qer) error { 24 | for idx, v := range s.qers { 25 | if v.qerID == q.qerID { 26 | s.qers[idx] = q 27 | return nil 28 | } 29 | } 30 | 31 | return ErrNotFound("QER") 32 | } 33 | 34 | // Int version of code present at https://github.com/juliangruber/go-intersect 35 | func Intersect(a []uint32, b []uint32) []uint32 { 36 | set := make([]uint32, 0) 37 | 38 | for i := 0; i < len(a); i++ { 39 | if contains(b, a[i]) { 40 | set = append(set, a[i]) 41 | } 42 | } 43 | 44 | return set 45 | } 46 | 47 | func contains(a []uint32, val uint32) bool { 48 | for i := 0; i < len(a); i++ { 49 | if val == a[i] { 50 | return true 51 | } 52 | } 53 | 54 | return false 55 | } 56 | 57 | func findItemIndex(slice []uint32, val uint32) int { 58 | for i := 0; i < len(slice); i++ { 59 | if val == slice[i] { 60 | return i 61 | } 62 | } 63 | 64 | return len(slice) 65 | } 66 | 67 | // MarkSessionQer : identify and Mark session QER with flag. 68 | func (s *PFCPSession) MarkSessionQer(qers []qer) { 69 | sessQerIDList := make([]uint32, 0) 70 | lastPdrIndex := len(s.pdrs) - 1 71 | // create search list with first pdr's qerlist */ 72 | sessQerIDList = append(sessQerIDList, s.pdrs[lastPdrIndex].qerIDList...) 73 | 74 | // If PDRs have no QERs, then no marking for session qers is needed. 75 | // If PDRS have one QER and all PDRs point to same QER, then consider it as application qer. 76 | // If number of QERS is 2 or more, then search for session QER 77 | if (len(sessQerIDList) < 1) || (len(qers) < 2) { 78 | logger.PfcpLog.Infoln("need atleast 1 QER in PDR or 2 QERs in session to mark session QER") 79 | return 80 | } 81 | 82 | // loop around all pdrs and find matching qers. 83 | for i := range s.pdrs { 84 | // match every qer in searchlist in pdr's qer list 85 | sList := Intersect(sessQerIDList, s.pdrs[i].qerIDList) 86 | if len(sList) == 0 { 87 | return 88 | } 89 | 90 | copy(sessQerIDList, sList) 91 | } 92 | 93 | // Loop through qer list and mark qer which matches 94 | // with entry in searchlist as sessionQos 95 | // if len(sessQerIDList) = 1 : use as matching session QER 96 | // if len(sessQerIDList) = 2 : loop and search for qer with 97 | // bigger MBR and choose as session QER 98 | // if len(sessQerIDList) = 0 : no session QER 99 | // if len(sessQerIDList) = 3 : TBD (UE level QER handling). 100 | // Currently handle same as len = 2 101 | var ( 102 | sessionIdx int 103 | sessionMbr uint64 104 | sessQerID uint32 105 | ) 106 | 107 | if len(sessQerIDList) > 3 { 108 | logger.PfcpLog.Warnln("qer id list size above 3 is not supported") 109 | } 110 | 111 | for idx, qer := range qers { 112 | if contains(sessQerIDList, qer.qerID) { 113 | if qer.ulGbr > 0 || qer.dlGbr > 0 { 114 | logger.InitLog.Infoln("do not consider qer with non zero gbr value for session qer") 115 | continue 116 | } 117 | 118 | if qer.ulMbr >= sessionMbr { 119 | sessionIdx = idx 120 | sessQerID = qer.qerID 121 | sessionMbr = qer.ulMbr 122 | } 123 | } 124 | } 125 | 126 | logger.PfcpLog.Infoln("session QER found. QER ID:", sessQerID) 127 | 128 | qers[sessionIdx].qosLevel = SessionQos 129 | 130 | for i := range s.pdrs { 131 | // remove common qerID from pdr's qer list 132 | idx := findItemIndex(s.pdrs[i].qerIDList, sessQerID) 133 | if idx != len(s.pdrs[i].qerIDList) { 134 | s.pdrs[i].qerIDList = append(s.pdrs[i].qerIDList[:idx], s.pdrs[i].qerIDList[idx+1:]...) 135 | s.pdrs[i].qerIDList = append(s.pdrs[i].qerIDList, sessQerID) 136 | } 137 | } 138 | } 139 | 140 | // RemoveQER removes qer from existing list of QERs in the session. 141 | func (s *PFCPSession) RemoveQER(id uint32) (*qer, error) { 142 | for idx, v := range s.qers { 143 | if v.qerID == id { 144 | s.qers = append(s.qers[:idx], s.qers[idx+1:]...) 145 | return &v, nil 146 | } 147 | } 148 | 149 | return nil, ErrNotFound("QER") 150 | } 151 | -------------------------------------------------------------------------------- /pfcpiface/sessions.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/omec-project/upf-epc/logger" 10 | "github.com/omec-project/upf-epc/pfcpiface/metrics" 11 | ) 12 | 13 | type PacketForwardingRules struct { 14 | pdrs []pdr 15 | fars []far 16 | qers []qer 17 | } 18 | 19 | // PFCPSession implements one PFCP session. 20 | type PFCPSession struct { 21 | localSEID uint64 22 | remoteSEID uint64 23 | metrics *metrics.Session 24 | PacketForwardingRules 25 | } 26 | 27 | func (p PacketForwardingRules) String() string { 28 | return fmt.Sprintf("PDRs=%v, FARs=%v, QERs=%v", p.pdrs, p.fars, p.qers) 29 | } 30 | 31 | // NewPFCPSession allocates an session with ID. 32 | func (pConn *PFCPConn) NewPFCPSession(rseid uint64) (PFCPSession, bool) { 33 | for i := 0; i < pConn.maxRetries; i++ { 34 | lseid := pConn.rng.Uint64() 35 | // Check if it already exists 36 | if _, ok := pConn.store.GetSession(lseid); ok { 37 | continue 38 | } 39 | 40 | s := PFCPSession{ 41 | localSEID: lseid, 42 | remoteSEID: rseid, 43 | PacketForwardingRules: PacketForwardingRules{ 44 | pdrs: make([]pdr, 0, MaxItems), 45 | fars: make([]far, 0, MaxItems), 46 | qers: make([]qer, 0, MaxItems), 47 | }, 48 | } 49 | s.metrics = metrics.NewSession(pConn.nodeID.remote) 50 | 51 | // Metrics update 52 | pConn.SaveSessions(s.metrics) 53 | 54 | return s, true 55 | } 56 | 57 | return PFCPSession{}, false 58 | } 59 | 60 | // RemoveSession removes session using lseid. 61 | func (pConn *PFCPConn) RemoveSession(session PFCPSession) { 62 | // Metrics update 63 | session.metrics.Delete() 64 | pConn.SaveSessions(session.metrics) 65 | 66 | if err := pConn.store.DeleteSession(session.localSEID); err != nil { 67 | logger.PfcpLog.Errorf("failed to delete PFCP session from store: %v", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pfcpiface/sessions_store.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | type SessionsStore interface { 7 | // PutSession modifies the PFCP Session data indexed by a given F-SEID or 8 | // inserts a new PFCP Session record, if it doesn't exist yet. 9 | PutSession(session PFCPSession) error 10 | // GetSession returns the PFCP Session data based on F-SEID. 11 | GetSession(fseid uint64) (PFCPSession, bool) 12 | // GetAllSessions returns all the PFCP Session records that are currently stored. 13 | GetAllSessions() []PFCPSession 14 | // DeleteSession removes a PFCP Session record indexed by F-SEID. 15 | DeleteSession(fseid uint64) error 16 | // DeleteAllSessions removes all PFCP sessions from the store. 17 | // Returns true on success. 18 | DeleteAllSessions() bool 19 | } 20 | -------------------------------------------------------------------------------- /pfcpiface/telemetry_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022-present Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/stretchr/testify/require" 9 | 10 | "net/http" 11 | "testing" 12 | ) 13 | 14 | // TODO: we currently need to reset the DefaultRegisterer between tests, as some 15 | // leave the registry in a bad state. Use custom registries to avoid global state. 16 | var backupGlobalRegistry prometheus.Registerer 17 | 18 | func saveReg() { 19 | backupGlobalRegistry = prometheus.DefaultRegisterer 20 | prometheus.DefaultRegisterer = prometheus.NewRegistry() 21 | } 22 | 23 | func restoreReg() { 24 | prometheus.DefaultRegisterer = backupGlobalRegistry 25 | } 26 | 27 | func Test_setupProm(t *testing.T) { 28 | t.Run("can setup prom multiple times with clearProm", func(t *testing.T) { 29 | saveReg() 30 | defer restoreReg() 31 | 32 | // TODO: use actual mocks 33 | upf := &upf{} 34 | node := NewPFCPNode(upf) 35 | 36 | uc, nc, err := setupProm(http.NewServeMux(), upf, node) 37 | require.NoError(t, err) 38 | 39 | clearProm(uc, nc) 40 | 41 | _, _, err = setupProm(http.NewServeMux(), upf, node) 42 | require.NoError(t, err) 43 | }) 44 | 45 | t.Run("cannot setup prom multiple times without clearProm", func(t *testing.T) { 46 | saveReg() 47 | defer restoreReg() 48 | 49 | // TODO: use actual mocks 50 | upf := &upf{} 51 | node := NewPFCPNode(upf) 52 | 53 | _, _, err := setupProm(http.NewServeMux(), upf, node) 54 | require.NoError(t, err) 55 | 56 | _, _, err = setupProm(http.NewServeMux(), upf, node) 57 | require.Error(t, err) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /pfcpiface/upf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "net" 8 | "time" 9 | 10 | "github.com/Showmax/go-fqdn" 11 | "github.com/omec-project/upf-epc/logger" 12 | ) 13 | 14 | // QosConfigVal : Qos configured value. 15 | type QosConfigVal struct { 16 | cbs uint32 17 | pbs uint32 18 | ebs uint32 19 | burstDurationMs uint32 20 | schedulePriority uint32 21 | } 22 | 23 | type SliceInfo struct { 24 | name string 25 | uplinkMbr uint64 26 | downlinkMbr uint64 27 | ulBurstBytes uint64 28 | dlBurstBytes uint64 29 | ueResList []UeResource 30 | } 31 | 32 | type UeResource struct { 33 | name string 34 | dnn string 35 | } 36 | 37 | type upf struct { 38 | enableUeIPAlloc bool 39 | enableEndMarker bool 40 | enableFlowMeasure bool 41 | enableGtpuMonitor bool 42 | accessIface string 43 | coreIface string 44 | ippoolCidr string 45 | n4addr string 46 | accessIP net.IP 47 | coreIP net.IP 48 | nodeID string 49 | ippool *IPPool 50 | peers []string 51 | dnn string 52 | reportNotifyChan chan uint64 53 | sliceInfo *SliceInfo 54 | readTimeout time.Duration 55 | fteidGenerator *FTEIDGenerator 56 | 57 | datapath 58 | maxReqRetries uint8 59 | respTimeout time.Duration 60 | enableHBTimer bool 61 | hbInterval time.Duration 62 | } 63 | 64 | // to be replaced with go-pfcp structs 65 | 66 | // Don't change these values. 67 | const ( 68 | tunnelGTPUPort = 2152 69 | 70 | // src-iface consts. 71 | core = 0x2 72 | access = 0x1 73 | 74 | // far-id specific directions. 75 | n3 = 0x0 76 | n6 = 0x1 77 | n9 = 0x2 78 | ) 79 | 80 | func (u *upf) isConnected() bool { 81 | return u.IsConnected(&u.accessIP) 82 | } 83 | 84 | func (u *upf) addSliceInfo(sliceInfo *SliceInfo) error { 85 | if sliceInfo == nil { 86 | return ErrInvalidArgument("sliceInfo", sliceInfo) 87 | } 88 | 89 | u.sliceInfo = sliceInfo 90 | 91 | return u.AddSliceInfo(sliceInfo) 92 | } 93 | 94 | func NewUPF(conf *Conf, fp datapath) *upf { 95 | var ( 96 | err error 97 | nodeID string 98 | hosts []string 99 | ) 100 | 101 | nodeID = conf.CPIface.NodeID 102 | if conf.CPIface.UseFQDN && nodeID == "" { 103 | nodeID, err = fqdn.FqdnHostname() 104 | if err != nil { 105 | logger.PfcpLog.Fatalln("unable to get hostname", err) 106 | } 107 | } 108 | 109 | // TODO: Delete this once CI config is fixed 110 | if nodeID != "" { 111 | hosts, err = net.LookupHost(nodeID) 112 | if err != nil { 113 | logger.PfcpLog.Fatalln("unable to resolve hostname", nodeID, err) 114 | } 115 | 116 | nodeID = hosts[0] 117 | } 118 | 119 | u := &upf{ 120 | enableUeIPAlloc: conf.CPIface.EnableUeIPAlloc, 121 | enableEndMarker: conf.EnableEndMarker, 122 | enableFlowMeasure: conf.EnableFlowMeasure, 123 | enableGtpuMonitor: conf.EnableGtpuPathMonitoring, 124 | accessIface: conf.AccessIface.IfName, 125 | coreIface: conf.CoreIface.IfName, 126 | ippoolCidr: conf.CPIface.UEIPPool, 127 | nodeID: nodeID, 128 | datapath: fp, 129 | dnn: conf.CPIface.Dnn, 130 | peers: conf.CPIface.Peers, 131 | reportNotifyChan: make(chan uint64, 1024), 132 | maxReqRetries: conf.MaxReqRetries, 133 | enableHBTimer: conf.EnableHBTimer, 134 | readTimeout: time.Second * time.Duration(conf.ReadTimeout), 135 | fteidGenerator: NewFTEIDGenerator(), 136 | n4addr: conf.N4Addr, 137 | } 138 | 139 | if len(conf.CPIface.Peers) > 0 { 140 | u.peers = make([]string, len(conf.CPIface.Peers)) 141 | nc := copy(u.peers, conf.CPIface.Peers) 142 | 143 | if nc == 0 { 144 | logger.PfcpLog.Warnln("failed to parse cpiface peers, PFCP Agent will not initiate connection to N4 peers.") 145 | } 146 | } 147 | 148 | if !conf.EnableP4rt { 149 | u.accessIP, err = GetUnicastAddressFromInterface(conf.AccessIface.IfName) 150 | if err != nil { 151 | logger.PfcpLog.Errorln(err) 152 | return nil 153 | } 154 | 155 | u.coreIP, err = GetUnicastAddressFromInterface(conf.CoreIface.IfName) 156 | if err != nil { 157 | logger.PfcpLog.Errorln(err) 158 | return nil 159 | } 160 | } 161 | 162 | u.respTimeout, err = time.ParseDuration(conf.RespTimeout) 163 | if err != nil { 164 | logger.PfcpLog.Fatalln("unable to parse resp_timeout") 165 | } 166 | 167 | if u.enableHBTimer { 168 | if conf.HeartBeatInterval != "" { 169 | u.hbInterval, err = time.ParseDuration(conf.HeartBeatInterval) 170 | if err != nil { 171 | logger.PfcpLog.Fatalln("unable to parse heart_beat_interval") 172 | } 173 | } 174 | } 175 | 176 | if u.enableUeIPAlloc { 177 | u.ippool, err = NewIPPool(u.ippoolCidr) 178 | if err != nil { 179 | logger.PfcpLog.Fatalln("ip pool init failed", err) 180 | } 181 | } 182 | 183 | u.SetUpfInfo(u, conf) 184 | 185 | return u 186 | } 187 | -------------------------------------------------------------------------------- /pfcpiface/utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | // Copyright 2022 Open Networking Foundation 4 | 5 | package pfcpiface 6 | 7 | import ( 8 | "encoding/binary" 9 | "net" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/omec-project/upf-epc/internal/p4constants" 14 | "github.com/omec-project/upf-epc/logger" 15 | ) 16 | 17 | // Bits type. 18 | type Bits uint8 19 | 20 | // Set Bits. 21 | func Set(b, flag Bits) Bits { return b | flag } 22 | 23 | // func Clear(b, flag Bits) Bits { return b &^ flag } 24 | // func Toggle(b, flag Bits) Bits { return b ^ flag } 25 | // func Has(b, flag Bits) bool { return b&flag != 0 } 26 | 27 | func setUeipFeature(features ...uint8) { 28 | if len(features) >= 3 { 29 | features[2] = features[2] | 0x04 30 | } 31 | } 32 | 33 | // Set the 5th bit of the first octet to 1. 34 | func setFTUPFeature(features ...uint8) { 35 | if len(features) >= 1 { 36 | features[0] = features[0] | 0x10 37 | } 38 | } 39 | 40 | func setEndMarkerFeature(features ...uint8) { 41 | if len(features) >= 2 { 42 | features[1] = features[1] | 0x01 43 | } 44 | } 45 | 46 | func has2ndBit(f uint8) bool { 47 | return (f&0x02)>>1 == 1 48 | } 49 | 50 | func has5thBit(f uint8) bool { 51 | return (f & 0x010) == 1 52 | } 53 | 54 | func inc(ip net.IP) { 55 | for j := len(ip) - 1; j >= 0; j-- { 56 | ip[j]++ 57 | if ip[j] > 0 { 58 | break 59 | } 60 | } 61 | } 62 | 63 | func ip2int(ip net.IP) uint32 { 64 | if len(ip) == 16 { 65 | return binary.BigEndian.Uint32(ip[12:16]) 66 | } 67 | 68 | return binary.BigEndian.Uint32(ip) 69 | } 70 | 71 | func ipMask2int(ip net.IPMask) uint32 { 72 | if len(ip) == 16 { 73 | return binary.BigEndian.Uint32(ip[12:16]) 74 | } 75 | 76 | return binary.BigEndian.Uint32(ip) 77 | } 78 | 79 | func hex2int(hexStr string) uint32 { 80 | // remove 0x suffix if found in the input string 81 | cleaned := strings.ReplaceAll(hexStr, "0x", "") 82 | 83 | // base 16 for hexadecimal 84 | result, _ := strconv.ParseUint(cleaned, 16, 32) 85 | 86 | return uint32(result) 87 | } 88 | 89 | func int2ip(nn uint32) net.IP { 90 | ip := make(net.IP, 4) 91 | binary.BigEndian.PutUint32(ip, nn) 92 | 93 | return ip 94 | } 95 | 96 | func maxUint64(x, y uint64) uint64 { 97 | if x < y { 98 | return y 99 | } 100 | 101 | return x 102 | } 103 | 104 | // Returns the bandwidth delay product for a given rate in kbps and duration in ms. 105 | func calcBurstSizeFromRate(kbps uint64, ms uint64) uint64 { 106 | return uint64((float64(kbps) * 1000 / 8) * (float64(ms) / 1000)) 107 | } 108 | 109 | // MustParseStrIP : parse IP address from config and fail on error. 110 | func MustParseStrIP(address string) *net.IPNet { 111 | ip, ipNet, err := net.ParseCIDR(address) 112 | if err != nil { 113 | logger.PfcpLog.Fatalf("unable to parse IP %v that we should parse", address) 114 | } 115 | 116 | logger.PfcpLog.Infoln("parsed IP:", ip) 117 | 118 | return ipNet 119 | } 120 | 121 | // GetUnicastAddressFromInterface returns a unicast IP address configured on the interface. 122 | func GetUnicastAddressFromInterface(interfaceName string) (net.IP, error) { 123 | iface, err := net.InterfaceByName(interfaceName) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | addresses, err := iface.Addrs() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | ip, _, err := net.ParseCIDR(addresses[0].String()) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return ip, nil 139 | } 140 | 141 | func GetSliceTCMeterIndex(sliceID uint8, TC uint8) (int64, error) { 142 | if sliceID >= (1 << p4constants.BitwidthMfSliceId) { 143 | return 0, ErrInvalidArgumentWithReason("SliceID", sliceID, "Slice ID higher than max supported slice ID") 144 | } 145 | 146 | if TC >= (1 << p4constants.BitwidthApTc) { 147 | return 0, ErrInvalidArgumentWithReason("TC", TC, "TC higher than max supported Traffic Class") 148 | } 149 | 150 | return int64((sliceID << 2) + (TC & 0b11)), nil 151 | } 152 | -------------------------------------------------------------------------------- /pfcpiface/utils_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "github.com/stretchr/testify/require" 8 | "github.com/wmnsk/go-pfcp/ie" 9 | 10 | "net" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | func GetLoopbackInterface() (net.Interface, error) { 16 | ifs, err := net.Interfaces() 17 | if err != nil { 18 | return net.Interface{}, err 19 | } 20 | 21 | for _, iface := range ifs { 22 | if (iface.Flags & net.FlagLoopback) != 0 { 23 | return iface, nil 24 | } 25 | } 26 | 27 | return net.Interface{}, ErrNotFound("No loopback interface found") 28 | } 29 | 30 | // This tests inherently depends on the host setup to a degree. 31 | // If it's not feasible to run, we will skip it. 32 | func TestGetUnicastAddressFromInterface(t *testing.T) { 33 | lb, err := GetLoopbackInterface() 34 | if err != nil { 35 | t.Skip("Skipping interface testing due to lack of suitable interfaces") 36 | } 37 | 38 | tests := []struct { 39 | name string 40 | interfaceName string 41 | want net.IP 42 | wantErr bool 43 | }{ 44 | {name: "loopback interface", interfaceName: lb.Name, want: net.ParseIP("127.0.0.1")}, 45 | {name: "nonexistent interface", interfaceName: "invalid1234", wantErr: true}, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run( 50 | tt.name, func(t *testing.T) { 51 | got, err := GetUnicastAddressFromInterface(tt.interfaceName) 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf( 54 | "GetUnicastAddressFromInterface() error = %v, wantErr %v", err, tt.wantErr, 55 | ) 56 | return 57 | } 58 | if !reflect.DeepEqual(got, tt.want) { 59 | t.Errorf("GetUnicastAddressFromInterface() got = %v, want %v", got, tt.want) 60 | } 61 | }, 62 | ) 63 | } 64 | } 65 | 66 | func TestGetSliceTcMeterIndex(t *testing.T) { 67 | tests := []struct { 68 | name string 69 | TC uint8 70 | sliceID uint8 71 | want int64 72 | wantErr bool 73 | }{ 74 | {name: "SliceID=0, TC=0", sliceID: 0, TC: 0, want: 0}, 75 | {name: "SliceID=3, TC=3", sliceID: 3, TC: 2, want: 14}, 76 | {name: "SliceID=15, TC=3", sliceID: 15, TC: 3, want: 63}, 77 | {name: "Big slice ID", sliceID: 16, TC: 3, wantErr: true}, 78 | {name: "Big Traffic Class", sliceID: 0, TC: 4, wantErr: true}, 79 | } 80 | 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | got, err := GetSliceTCMeterIndex(tt.sliceID, tt.TC) 84 | if (err != nil) != tt.wantErr { 85 | t.Errorf( 86 | "GetSliceTcMeterIndex() error = %v, wantErr %v", err, tt.wantErr, 87 | ) 88 | return 89 | } 90 | require.Equal(t, tt.want, got) 91 | }, 92 | ) 93 | } 94 | } 95 | 96 | func TestSetUeipFeature(t *testing.T) { 97 | features := make([]uint8, 4) 98 | 99 | setUeipFeature(features...) 100 | 101 | ie := ie.NewUPFunctionFeatures(features...) 102 | hasUeIPAlloc := ie.HasUEIP() 103 | if !hasUeIPAlloc { 104 | t.Errorf("Expected UEIPAlloc to be set") 105 | } 106 | } 107 | 108 | func TestSetFTUPFeature(t *testing.T) { 109 | features := make([]uint8, 4) 110 | 111 | setFTUPFeature(features...) 112 | 113 | ie := ie.NewUPFunctionFeatures(features...) 114 | hasFTUP := ie.HasFTUP() 115 | if !hasFTUP { 116 | t.Errorf("Expected FTUP to be set") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pfcpiface/web_service.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | 4 | package pfcpiface 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "math" 10 | "net/http" 11 | 12 | "github.com/omec-project/upf-epc/logger" 13 | ) 14 | 15 | // NetworkSlice ... Config received for slice rates and DNN. 16 | type NetworkSlice struct { 17 | SliceName string `json:"sliceName"` 18 | SliceQos SliceQos `json:"sliceQos"` 19 | UeResInfo []UeResInfo `json:"ueResourceInfo"` 20 | } 21 | 22 | // SliceQos ... Slice level QOS rates. 23 | type SliceQos struct { 24 | UplinkMbr uint64 `json:"uplinkMbr"` 25 | DownlinkMbr uint64 `json:"downlinkMbr"` 26 | BitrateUnit string `json:"bitrateUnit"` 27 | UlBurstBytes uint64 `json:"uplinkBurstSize"` 28 | DlBurstBytes uint64 `json:"downlinkBurstSize"` 29 | } 30 | 31 | // UeResInfo ... UE Pool and DNN info. 32 | type UeResInfo struct { 33 | Dnn string `json:"dnn"` 34 | Name string `json:"uePoolId"` 35 | } 36 | 37 | type ConfigHandler struct { 38 | upf *upf 39 | } 40 | 41 | func setupConfigHandler(mux *http.ServeMux, upf *upf) { 42 | cfgHandler := ConfigHandler{upf: upf} 43 | mux.Handle("/v1/config/network-slices", &cfgHandler) 44 | } 45 | 46 | func (c *ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 47 | logger.PfcpLog.Infoln("handle http request for /v1/config/network-slices") 48 | 49 | switch r.Method { 50 | case "PUT": 51 | fallthrough 52 | case "POST": 53 | body, err := io.ReadAll(r.Body) 54 | if err != nil { 55 | logger.PfcpLog.Errorln("http req read body failed") 56 | sendHTTPResp(http.StatusBadRequest, w) 57 | } 58 | 59 | logger.PfcpLog.Debugln(string(body)) 60 | 61 | var nwSlice NetworkSlice 62 | 63 | err = json.Unmarshal(body, &nwSlice) 64 | if err != nil { 65 | logger.PfcpLog.Errorln("Json unmarshal failed for http request") 66 | sendHTTPResp(http.StatusBadRequest, w) 67 | } 68 | 69 | handleSliceConfig(&nwSlice, c.upf) 70 | sendHTTPResp(http.StatusCreated, w) 71 | default: 72 | logger.PfcpLog.Infoln(w, "sorry, only PUT and POST methods are supported") 73 | sendHTTPResp(http.StatusMethodNotAllowed, w) 74 | } 75 | } 76 | 77 | func sendHTTPResp(status int, w http.ResponseWriter) { 78 | w.WriteHeader(status) 79 | w.Header().Set("Content-Type", "application/json") 80 | 81 | resp := make(map[string]string) 82 | 83 | switch status { 84 | case http.StatusCreated: 85 | resp["message"] = "Status Created" 86 | default: 87 | resp["message"] = "Failed to add slice" 88 | } 89 | 90 | jsonResp, err := json.Marshal(resp) 91 | if err != nil { 92 | logger.PfcpLog.Errorln("error happened in JSON marshal:", err) 93 | } 94 | 95 | _, err = w.Write(jsonResp) 96 | if err != nil { 97 | logger.PfcpLog.Errorln("http response write failed:", err) 98 | } 99 | } 100 | 101 | // calculateBitRates : Default bit rate is Mbps. 102 | func calculateBitRates(mbr uint64, rate string) uint64 { 103 | var val int64 104 | 105 | switch rate { 106 | case "bps": 107 | return mbr 108 | case "Kbps": 109 | val = int64(mbr) * KB 110 | case "Gbps": 111 | val = int64(mbr) * GB 112 | case "Mbps": 113 | fallthrough 114 | default: 115 | val = int64(mbr) * MB 116 | } 117 | 118 | if val > 0 { 119 | return uint64(val) 120 | } else { 121 | return uint64(math.MaxInt64) 122 | } 123 | } 124 | 125 | func handleSliceConfig(nwSlice *NetworkSlice, upf *upf) { 126 | logger.PfcpLog.Infoln("handle slice config:", nwSlice.SliceName) 127 | 128 | ulMbr := calculateBitRates(nwSlice.SliceQos.UplinkMbr, 129 | nwSlice.SliceQos.BitrateUnit) 130 | dlMbr := calculateBitRates(nwSlice.SliceQos.DownlinkMbr, 131 | nwSlice.SliceQos.BitrateUnit) 132 | sliceInfo := SliceInfo{ 133 | name: nwSlice.SliceName, 134 | uplinkMbr: ulMbr, 135 | downlinkMbr: dlMbr, 136 | ulBurstBytes: nwSlice.SliceQos.UlBurstBytes, 137 | dlBurstBytes: nwSlice.SliceQos.DlBurstBytes, 138 | } 139 | 140 | if len(nwSlice.UeResInfo) > 0 { 141 | sliceInfo.ueResList = make([]UeResource, 0) 142 | 143 | for _, ueRes := range nwSlice.UeResInfo { 144 | var ueResInfo UeResource 145 | ueResInfo.dnn = ueRes.Dnn 146 | ueResInfo.name = ueRes.Name 147 | sliceInfo.ueResList = append(sliceInfo.ueResList, ueResInfo) 148 | } 149 | } 150 | 151 | err := upf.addSliceInfo(&sliceInfo) 152 | if err != nil { 153 | logger.PfcpLog.Errorln("adding slice info to datapath failed:", err) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pkg/fake_bess/fake_bess.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package fake_bess 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/omec-project/upf-epc/pfcpiface/bess_pb" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type FakeBESS struct { 14 | grpcServer *grpc.Server 15 | service *fakeBessService 16 | } 17 | 18 | // NewFakeBESS creates a new fake BESS gRPC server. Its modules can be programmed in the same way 19 | // as the real BESS and keep track of their state. 20 | func NewFakeBESS() *FakeBESS { 21 | return &FakeBESS{ 22 | service: newFakeBESSService(), 23 | } 24 | } 25 | 26 | // Run starts and runs the BESS gRPC server on the given address. Blocking until Stop is called. 27 | func (b *FakeBESS) Run(address string) error { 28 | listener, err := net.Listen("tcp", address) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | b.grpcServer = grpc.NewServer() 34 | bess_pb.RegisterBESSControlServer(b.grpcServer, b.service) 35 | 36 | // Blocking 37 | err = b.grpcServer.Serve(listener) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // Stop the BESS gRPC server. 46 | func (b *FakeBESS) Stop() { 47 | b.grpcServer.Stop() 48 | } 49 | 50 | func (b *FakeBESS) GetPdrTableEntries() (entries map[uint32][]FakePdr) { 51 | entries = make(map[uint32][]FakePdr) 52 | msgs := b.service.GetOrAddModule(pdrLookupModuleName).GetState() 53 | for _, m := range msgs { 54 | e, ok := m.(*bess_pb.WildcardMatchCommandAddArg) 55 | if !ok { 56 | panic("unexpected message type") 57 | } 58 | pdr := UnmarshalPdr(e) 59 | entries[pdr.PdrID] = append(entries[pdr.PdrID], pdr) 60 | } 61 | 62 | return 63 | } 64 | 65 | func (b *FakeBESS) GetFarTableEntries() (entries map[uint32]FakeFar) { 66 | entries = make(map[uint32]FakeFar) 67 | msgs := b.service.GetOrAddModule(farLookupModuleName).GetState() 68 | for _, m := range msgs { 69 | e, ok := m.(*bess_pb.ExactMatchCommandAddArg) 70 | if !ok { 71 | panic("unexpected message type") 72 | } 73 | far := UnmarshalFar(e) 74 | entries[far.FarID] = far 75 | } 76 | return 77 | } 78 | 79 | // Session QERs are missing a QerID and are therefore returned as a slice, not map. 80 | func (b *FakeBESS) GetSessionQerTableEntries() (entries []FakeQer) { 81 | msgs := b.service.GetOrAddModule(sessionQerModuleName).GetState() 82 | for _, m := range msgs { 83 | e, ok := m.(*bess_pb.QosCommandAddArg) 84 | if !ok { 85 | panic("unexpected message type") 86 | } 87 | entries = append(entries, UnmarshalSessionQer(e)) 88 | } 89 | return 90 | } 91 | 92 | func (b *FakeBESS) GetAppQerTableEntries() (entries []FakeQer) { 93 | msgs := b.service.GetOrAddModule(appQerModuleName).GetState() 94 | for _, m := range msgs { 95 | e, ok := m.(*bess_pb.QosCommandAddArg) 96 | if !ok { 97 | panic("unexpected message type") 98 | } 99 | entries = append(entries, UnmarshalAppQer(e)) 100 | } 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 Intel Corporation 3 | // Copyright 2022 Open Networking Foundation 4 | 5 | package utils 6 | 7 | import ( 8 | "encoding/binary" 9 | "net" 10 | ) 11 | 12 | func Uint32ToIp4(nn uint32) net.IP { 13 | ip := make(net.IP, 4) 14 | binary.BigEndian.PutUint32(ip, nn) 15 | 16 | return ip 17 | } 18 | 19 | func Ip4ToUint32(ip net.IP) uint32 { 20 | return binary.BigEndian.Uint32(ip.To4()) 21 | } 22 | 23 | func MaxUint16(x, y uint16) uint16 { 24 | if x < y { 25 | return y 26 | } 27 | 28 | return x 29 | } 30 | 31 | func MinUint16(x, y uint16) uint16 { 32 | if MaxUint16(x, y) == x { 33 | return y 34 | } 35 | 36 | return x 37 | } 38 | 39 | func Uint8Has3rdBit(f uint8) bool { 40 | return (f&0x04)>>2 == 1 41 | } 42 | 43 | func Uint8Has2ndBit(f uint8) bool { 44 | return (f&0x02)>>1 == 1 45 | } 46 | 47 | func Uint8Has1stBit(f uint8) bool { 48 | return (f & 0x01) == 1 49 | } 50 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | "net" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestInt2ip(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | args uint32 19 | want net.IP 20 | }{ 21 | {name: "zero", args: 0, want: net.IPv4zero.To4()}, 22 | {name: "plain", args: 0x0a000001, want: net.ParseIP("10.0.0.1").To4()}, 23 | } 24 | for _, tt := range tests { 25 | t.Run( 26 | tt.name, func(t *testing.T) { 27 | got := Uint32ToIp4(tt.args) 28 | require.Equal(t, tt.want, got) 29 | }, 30 | ) 31 | } 32 | } 33 | 34 | func TestIp2int(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | ip net.IP 38 | want uint32 39 | }{ 40 | {name: "zero", ip: net.IPv4zero.To4(), want: 0}, 41 | {name: "plain", ip: net.ParseIP("10.0.0.1").To4(), want: 0x0a000001}, 42 | {name: "v6 mapped v4", ip: net.ParseIP("::ffff:10.0.0.1"), want: 0x0a000001}, 43 | } 44 | for _, tt := range tests { 45 | t.Run( 46 | tt.name, func(t *testing.T) { 47 | if got := Ip4ToUint32(tt.ip); got != tt.want { 48 | t.Errorf("Ip4ToUint32() = %v, want %v", got, tt.want) 49 | } 50 | }, 51 | ) 52 | } 53 | } 54 | 55 | func TestIp2int2IpTransitive(t *testing.T) { 56 | tests := []uint32{ 57 | 0, 58 | 1, 59 | math.MaxUint32, 60 | 0x0a000001, 61 | } 62 | for _, i := range tests { 63 | t.Run(fmt.Sprint(i), func(t *testing.T) { 64 | ip := Uint32ToIp4(i) 65 | got := Ip4ToUint32(ip) 66 | require.Equal(t, i, got, "value %v failed transitive conversion with intermediate ip %v", ip) 67 | }, 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ptf/.env: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | # This file specifies the default values for environment variables used 5 | # throughout this project. Default values can be overridden via the command 6 | # line, for example: 7 | # 8 | # export UPF_ADDR="192.168.0.1" 9 | 10 | 11 | # Contains all dependencies for PTF tests 12 | TESTER_DOCKER_IMG=bess-upf-ptf 13 | # Address of the server running the UPF 14 | UPF_ADDR=${UPF_ADDR:-"192.168.150.1:10514"} 15 | # Address of the server running TRex for traffic generation 16 | TREX_ADDR=${TREX_ADDR:-"192.168.150.2"} 17 | -------------------------------------------------------------------------------- /ptf/Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | # Docker image to run PTF-based tests 5 | 6 | ARG SCAPY_VER=2.4.5 7 | ARG PTF_VER=c5299ea2e27386653209af458757b3b15e5dec5d 8 | ARG TREX_VER=2.92-scapy-2.4.5 9 | ARG TREX_EXT_LIBS=/external_libs 10 | ARG TREX_LIBS=/trex_python 11 | ARG UNITTEST_XML_REPORTING_VER=3.0.4 12 | ARG PROTOBUF_VER=3.20 13 | # Install dependencies for general PTF test definitions 14 | FROM ubuntu:22.04 AS ptf-deps 15 | 16 | ARG SCAPY_VER 17 | ARG PTF_VER 18 | ARG UNITTEST_XML_REPORTING_VER 19 | ARG PROTOBUF_VER 20 | ARG GRPC_VER 21 | 22 | RUN apt-get update && \ 23 | apt-get install -y --no-install-recommends \ 24 | python3 \ 25 | python3-pip \ 26 | python3-setuptools \ 27 | git && \ 28 | apt-get clean && \ 29 | rm -rf /var/lib/apt/lists/* 30 | 31 | RUN pip3 install --no-cache-dir --root /python_output \ 32 | git+https://github.com/p4lang/ptf@$PTF_VER \ 33 | protobuf==$PROTOBUF_VER \ 34 | grpcio \ 35 | unittest-xml-reporting==$UNITTEST_XML_REPORTING_VER 36 | 37 | # Install TRex traffic gen and library for TRex API 38 | FROM alpine:3.21 AS trex-builder 39 | ARG TREX_VER 40 | ARG TREX_EXT_LIBS 41 | ARG TREX_LIBS 42 | 43 | ENV TREX_SCRIPT_DIR=/trex-core-${TREX_VER}/scripts 44 | 45 | RUN apk update && apk add --no-cache -U wget && \ 46 | wget https://github.com/stratum/trex-core/archive/v${TREX_VER}.zip && \ 47 | unzip -qq v${TREX_VER}.zip && \ 48 | mkdir -p /output/${TREX_EXT_LIBS} && \ 49 | mkdir -p /output/${TREX_LIBS} && \ 50 | cp -r ${TREX_SCRIPT_DIR}/automation/trex_control_plane/interactive/* /output/${TREX_LIBS} && \ 51 | cp -r ${TREX_SCRIPT_DIR}/external_libs/* /output/${TREX_EXT_LIBS} && \ 52 | cp -r ${TREX_SCRIPT_DIR}/automation/trex_control_plane/stf/trex_stf_lib /output/${TREX_LIBS} 53 | 54 | # Synthesize all dependencies for runtime 55 | FROM ubuntu:22.04 56 | 57 | ARG TREX_EXT_LIBS 58 | ARG TREX_LIBS 59 | ARG SCAPY_VER 60 | 61 | RUN apt-get update && \ 62 | apt-get install -y --no-install-recommends \ 63 | make \ 64 | net-tools \ 65 | python3 \ 66 | python3-setuptools \ 67 | iproute2 \ 68 | tcpdump \ 69 | dumb-init \ 70 | python3-dev \ 71 | build-essential \ 72 | python3-pip \ 73 | wget \ 74 | netbase && \ 75 | apt-get clean && \ 76 | rm -rf /var/lib/apt/lists/* 77 | RUN pip3 install --no-cache-dir \ 78 | scipy \ 79 | numpy \ 80 | matplotlib \ 81 | pyyaml 82 | 83 | ENV TREX_EXT_LIBS=${TREX_EXT_LIBS} 84 | ENV PYTHONPATH=${TREX_EXT_LIBS}:${TREX_LIBS} 85 | 86 | COPY --from=trex-builder /output / 87 | COPY --from=ptf-deps /python_output / 88 | 89 | # Install custom scapy version from TRex so PTF tests can access certain scapy features 90 | # TODO: Move to a newer Scapy version compatible with latest Ubuntu versions 91 | WORKDIR ${TREX_EXT_LIBS}/scapy-${SCAPY_VER} 92 | RUN python3 setup.py install \ 93 | && ldconfig 94 | 95 | ENTRYPOINT [] 96 | -------------------------------------------------------------------------------- /ptf/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2020-present Open Networking Foundation 3 | 4 | # generates python protobuf files and builds ptf docker image 5 | build: 6 | cd .. && make py-pb 7 | docker build -t bess-upf-ptf . 8 | 9 | # removes generated python protobuf files 10 | clean: 11 | rm -v lib/*pb2* 12 | rm -rvf lib/ports/ 13 | -------------------------------------------------------------------------------- /ptf/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # PTF tests for BESS-UPF 4 | 5 | ## Overview 6 | 7 | The aim of implementing a test framework for UPF is to create a 8 | developer-friendly infrastructure for creating either single-packet or 9 | high-speed tests that assess UPF features at a component level. 10 | 11 | This "component-level" is achieved by *bypassing* calls to the PFCP 12 | agent, in favor of communicating with BESS directly via gRPC. 13 | 14 | ![Routes](docs/upf-access.svg) 15 | 16 | This figure illustrates two options for communicating with the UPF. In 17 | this framework, we opt for **BESS gRPC calls** instead of calls to the PFCP 18 | agent because they allow direct communication between the framework and 19 | the BESS instance for both installing rules and reading metrics. 20 | 21 | ## Workflow 22 | Tests require two separate machines to run, since both TRex and UPF 23 | use DPDK. Currently, the test workflow is as such: 24 | 25 | ![Test](docs/test-run.svg) 26 | 27 | In **step 1**, rules are installed onto the UPF instance by the test 28 | framework via BESS gRPC messages. 29 | 30 | In **step 2**, TRex or Scapy (depending on the type of test case) 31 | generates traffic to the UPF across NICs and physical links. 32 | 33 | In **step 3**, traffic routes through the UPF and back to the machine 34 | hosting TRex, where results are asserted. 35 | 36 | ## Required Tools/Components 37 | * [PTF](https://github.com/p4lang/PTF) (Packet Testing Framework): a 38 | data plane testing framework written in Python 39 | * [TRex](https://github.com/cisco-system-traffic-generator/trex-core): a 40 | high-speed traffic generator built on top of DPDK, containing a Python API 41 | 42 | ## Directory Structure 43 | ### config 44 | This directory contains YAML config file definition for TRex along with 45 | other personalized config files 46 | ### lib 47 | This directory contains general purpose libraries and classes to be imported in 48 | PTF test definitions 49 | ### tests 50 | This directory contains all of the test case definitions (written in Python), as 51 | well as scripts for running them in a test environment. We currently provide two 52 | general types of test cases: 53 | * `unary`: tests are *single packet* tests that assess UPF performance in 54 | specific scenarios. Packets are crafted and sent to the UPF using the `Scapy` 55 | packet library. 56 | > *Example*: a unary test could use Scapy to send a single non-encapsulated 57 | UDP packet to the core interface of the UPF, and assert that a 58 | GTP-encapsulated packet was received from the access interface 59 | * `linerate`: tests assess the UPF's performance in certain scenarios 60 | at high speeds. This allows UPF features to be verified that they perform as 61 | expected in an environment more representative of *production level*. Traffic is 62 | generated using the [TRex Python API](https://github.com/cisco-system-traffic-generator/trex-core/blob/master/doc/trex_cookbook.asciidoc). 63 | > *Example*: a line rate test could assert the baseline throughput, latency, 64 | etc. of the UPF is as expected when handling high-speed downlink traffic from 65 | 10,000 unique UEs 66 | 67 | ## Run tests 68 | The run script assumes that the TRex daemon server and the UPF 69 | instance are already running on their respective machines. Please see 70 | [here](../docs/INSTALL.md#configuration-dpdk-mode) for instructions to deploy 71 | the UPF in DPDK mode. Note that the following additional changes are required 72 | in the `conf/upf.jsonc` file: `"measure_flow": true`, N3 interface set to 73 | `"ifname": "access"` and N6 interface set to `"ifname": "core"`. 74 | To install TRex onto your server, please refer to the 75 | [TRex installation guide](https://trex-tgn.cisco.com/trex/doc/trex_manual.html#_download_and_installation) 76 | 77 | ### Steps 78 | 1. Update the following files accordingly to route traffic to the UPF and vice versa. 79 | * `ptf/.env` file updated with `UPF_ADDR` and `TREX_ADDR` parameters 80 | * `ptf/config/trex-cfg-for-ptf.yaml` file updated with proper values for 81 | `interfaces`, `port_info`, and `platform` parameters 82 | * `ptf/tests/linerate/common.py` file updated with proper MAC address values for 83 | `TREX_SRC_MAC`, `UPF_CORE_MAC`, and `UPF_ACCESS_MAC` 84 | 85 | 2. Move into the `ptf` directory 86 | ```bash 87 | cd ptf 88 | ``` 89 | 90 | 3. Generate BESS Python protobuf files for gRPC library and PTF Dockerfile image 91 | build dependencies: 92 | ```bash 93 | make build 94 | ``` 95 | 96 | 4. Run PTF tests using the `run_tests` script: 97 | ```bash 98 | ./run_tests -t [test-dir] [optional: filename/filename.test_case] 99 | ``` 100 | 101 | ### Examples 102 | To run all test cases in the `unary/` directory: 103 | ```bash 104 | ./run_tests -t tests/unary 105 | ``` 106 | To run a specific test case: 107 | ```bash 108 | ./run_tests -t tests/linerate/ baseline.DownlinkPerformanceBaselineTest 109 | ./run_tests -t tests/linerate/ mbr 110 | ./run_tests -t tests/linerate/ qos_metrics 111 | ``` 112 | Note: If the above fails, `sudo` may be needed 113 | -------------------------------------------------------------------------------- /ptf/config/trex-cfg-for-ptf.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2020-present Open Networking Foundation. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | - version: 2 5 | port_limit: 2 6 | interfaces: ['86:00.0', '86:00.1'] 7 | port_bandwidth_gb: 10 8 | c: 2 9 | port_info: 10 | - src_mac: 40:a6:b7:20:c8:24 11 | dest_mac: 40:a6:b7:20:4f:b8 12 | - src_mac: 40:a6:b7:20:c8:25 13 | dest_mac: 40:a6:b7:20:4f:b9 14 | memory: 15 | mbuf_64 : 4096 16 | mbuf_128 : 512 17 | mbuf_256 : 256 18 | mbuf_512 : 128 19 | mbuf_1024 : 256 20 | mbuf_2048 : 128 21 | traffic_mbuf_64 : 4096 22 | traffic_mbuf_128 : 512 23 | traffic_mbuf_256 : 256 24 | traffic_mbuf_512 : 128 25 | traffic_mbuf_1024 : 256 26 | traffic_mbuf_2048 : 128 27 | dp_flows : 4096 28 | platform: 29 | master_thread_id: 10 30 | rx_thread_id: 11 # replaces latency_thread_id 31 | dual_if: 32 | - socket: 0 33 | threads: [12, 13] 34 | -------------------------------------------------------------------------------- /ptf/docs/test-run.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /ptf/docs/upf-access.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2019-present Intel Corporation 2 | SPDX-FileCopyrightText: 2020-present Open Networking Foundation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /ptf/jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Copyright 2021 Open Networking Foundation 5 | 6 | # This file is executed by the Jenkins job defined at 7 | # https://gerrit.onosproject.org/plugins/gitiles/ci-management/+/refs/heads/master/jjb/pipeline/bess-upf-linerate.groovy 8 | # https://gerrit.onosproject.org/plugins/gitiles/ci-management/+/refs/heads/master/jjb/templates/bess-upf-job.yaml 9 | 10 | set -eux -o pipefail 11 | 12 | make build 13 | ./run_tests -t tests/linerate 14 | -------------------------------------------------------------------------------- /ptf/lib/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2022-present Open Networking Foundation 3 | 4 | # protoc-generated files 5 | *_pb2.py 6 | *_pb2_grpc.py 7 | -------------------------------------------------------------------------------- /ptf/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omec-project/upf/044210cc9dff56fc89aa8b6ff8a17ace19a468b9/ptf/lib/__init__.py -------------------------------------------------------------------------------- /ptf/lib/pkt_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022-present Open Networking Foundation. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from scapy.contrib.gtp import GTP_U_Header, GTPPDUSessionContainer 5 | from scapy.layers.inet import IP, UDP 6 | from scapy.layers.l2 import Ether 7 | 8 | GTPU_PORT = 2152 9 | 10 | 11 | def pkt_add_gtpu( 12 | pkt, 13 | out_ipv4_src, 14 | out_ipv4_dst, 15 | teid, 16 | sport=GTPU_PORT, 17 | dport=GTPU_PORT, 18 | ext_psc_type=None, 19 | ext_psc_qfi=None, 20 | ): 21 | """ 22 | Encapsulates the given pkt with GTPU tunnel headers. 23 | """ 24 | gtp_pkt = ( 25 | Ether(src=pkt[Ether].src, dst=pkt[Ether].dst) 26 | / IP( 27 | src=out_ipv4_src, 28 | dst=out_ipv4_dst, 29 | tos=0, 30 | id=0x1513, 31 | flags=0, 32 | frag=0, 33 | ) 34 | / UDP(sport=sport, dport=dport, chksum=0) 35 | / GTP_U_Header(gtp_type=255, teid=teid) 36 | ) 37 | if ext_psc_type is not None: 38 | # Add QoS Flow Identifier (QFI) as an extension header (required for 5G RAN) 39 | gtp_pkt = gtp_pkt / GTPPDUSessionContainer(type=ext_psc_type, QFI=ext_psc_qfi) 40 | return gtp_pkt / pkt[Ether].payload 41 | -------------------------------------------------------------------------------- /ptf/lib/trex_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | import ptf.testutils as testutils 5 | from ptf.base_tests import BaseTest 6 | from trex.stl.api import STLClient 7 | 8 | 9 | class TrexTest(BaseTest): 10 | """ 11 | Base test for setting up and tearing down TRex client instance for 12 | linerate tests. 13 | """ 14 | 15 | def setUp(self): 16 | super(TrexTest, self).setUp() 17 | trex_server_addr = testutils.test_param_get("trex_server_addr") 18 | self.trex_client = STLClient(server=trex_server_addr) 19 | self.trex_client.connect() 20 | self.trex_client.acquire() 21 | self.reset() 22 | 23 | def reset(self): 24 | self.trex_client.reset() # Resets configs from all ports 25 | self.trex_client.clear_stats() # Clear status from all ports 26 | # Put all ports to promiscuous mode, otherwise they will drop all 27 | # incoming packets if the destination mac is not the port mac address. 28 | self.trex_client.set_port_attr( 29 | self.trex_client.get_all_ports(), promiscuous=True 30 | ) 31 | 32 | def tearDown(self): 33 | print("Tearing down STLClient...") 34 | self.trex_client.stop() 35 | self.trex_client.release() 36 | self.trex_client.disconnect() 37 | super(TrexTest, self).tearDown() 38 | -------------------------------------------------------------------------------- /ptf/run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Copyright 2021 Open Networking Foundation 5 | 6 | set -eux -o pipefail 7 | 8 | TEST_DIR="$(pwd)" 9 | 10 | # Read in parameters following style: https://google.github.io/styleguide/shellguide.html#case-statement 11 | while getopts 't:' flag; do 12 | case "${flag}" in 13 | t) testdir="${OPTARG}" ;; 14 | *) error "Unexpected option ${flag}" ;; 15 | esac 16 | done 17 | 18 | testName="${3:-""}" 19 | # assign random number to each instance of ptf container 20 | runName=ptf-${RANDOM} 21 | 22 | source .env 23 | echo "*** Starting tests in container (${runName})..." 24 | 25 | # Do not attach stdin if running in an environment without it (e.g. Jenkins) 26 | it=$(test -t 0 && echo "-it" || echo "-t") 27 | 28 | # run PTF command from script in test container with all necessary dependencies 29 | docker run --name "${runName}" "${it}" \ 30 | --privileged \ 31 | -v "${TEST_DIR}":/upf-tests/ \ 32 | -v "${TEST_DIR}/log":/tmp \ 33 | "${TESTER_DOCKER_IMG}" \ 34 | python3 -u /upf-tests/lib/ptf_runner.py \ 35 | --ptf-dir /upf-tests/"${testdir}" \ 36 | ${TREX_PARAMS:-} \ 37 | --trex-address "${TREX_ADDR}" \ 38 | --bess-address "${UPF_ADDR}" \ 39 | --trex-config /upf-tests/config/trex-cfg-for-ptf.yaml \ 40 | --xunit \ 41 | --xunit-dir /tmp/ptf-logs \ 42 | ${testName} 43 | -------------------------------------------------------------------------------- /ptf/tests/linerate/baseline.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | import time 5 | from ipaddress import IPv4Address 6 | from pprint import pprint 7 | 8 | import ptf.testutils as testutils 9 | from grpc_test import * 10 | from pkt_utils import GTPU_PORT 11 | from trex_stl_lib.api import * 12 | from trex_test import TrexTest 13 | from trex_utils import * 14 | 15 | from common import * 16 | 17 | class DownlinkPerformanceBaselineTest(TrexTest, GrpcTest): 18 | """ 19 | Performance baseline linerate test generating downlink traffic at 1 Mpps 20 | with 10k UE IPs, asserting expected performance of BESS-UPF as reported by 21 | TRex traffic generator. 22 | """ 23 | 24 | @autocleanup 25 | def runTest(self): 26 | n3TEID = 0 27 | endIP = UE_IP_START + UE_COUNT - 1 28 | 29 | # program UPF for downlink traffic by installing PDRs and FARs 30 | print("Installing PDRs and FARs...") 31 | for i in range(UE_COUNT): 32 | # install N6 DL PDR to match UE dst IP 33 | pdrDown = self.createPDR( 34 | srcIface=CORE, 35 | dstIP=int(UE_IP_START + i), 36 | srcIfaceMask=0xFF, 37 | dstIPMask=0xFFFFFFFF, 38 | precedence=255, 39 | fseID=n3TEID + i + 1, # start from 1 40 | ctrID=0, 41 | farID=i, 42 | qerIDList=[N6, 1], 43 | needDecap=0, 44 | ) 45 | self.addPDR(pdrDown) 46 | 47 | # install N6 DL FAR for encap 48 | farDown = self.createFAR( 49 | farID=i, 50 | fseID=n3TEID + i + 1, # start from 1 51 | applyAction=ACTION_FORWARD, 52 | dstIntf=DST_ACCESS, 53 | tunnelType=0x1, 54 | tunnelIP4Src=int(N3_IP), 55 | tunnelIP4Dst=int(GNB_IP), 56 | tunnelTEID=0, 57 | tunnelPort=GTPU_PORT, 58 | ) 59 | self.addFAR(farDown) 60 | 61 | # install N6 DL/UL application QER 62 | qer = self.createQER( 63 | gate=GATE_UNMETER, 64 | qerID=N6, 65 | fseID=n3TEID + i + 1, # start from 1 66 | qfi=9, 67 | ulGbr=0, 68 | ulMbr=0, 69 | dlGbr=0, 70 | dlMbr=0, 71 | burstDurationMs=10, 72 | ) 73 | self.addApplicationQER(qer) 74 | 75 | # set up trex to send traffic thru UPF 76 | print("Setting up TRex client...") 77 | vm = STLVM() 78 | vm.var( 79 | name="dst", 80 | min_value=str(UE_IP_START), 81 | max_value=str(endIP), 82 | size=4, 83 | op="random", 84 | ) 85 | vm.write(fv_name="dst", pkt_offset="IP.dst") 86 | vm.fix_chksum() 87 | 88 | eth = Ether(dst=UPF_CORE_MAC, src=TREX_SRC_MAC) 89 | ip = IP(src=PDN_IP, id=0) 90 | udp = UDP(sport=10002, dport=10001, chksum=0) 91 | pkt = eth/ip/udp 92 | 93 | stream = STLStream( 94 | packet=STLPktBuilder(pkt=pkt, vm=vm), 95 | mode=STLTXCont(pps=RATE), 96 | flow_stats=STLFlowLatencyStats(pg_id=0), 97 | ) 98 | 99 | # Wait for sometime before starting traffic. Sometimes the ports are 100 | # taking some time to become active. Otherwise, the test will 101 | # fail due to port DOWN state 102 | time.sleep(20) 103 | 104 | self.trex_client.add_streams(stream, ports=[UPF_CORE_PORT]) 105 | 106 | print("Running traffic...") 107 | s_time = time.time() 108 | self.trex_client.start( 109 | ports=[UPF_CORE_PORT], 110 | mult="1", 111 | duration=DURATION, 112 | ) 113 | self.trex_client.wait_on_traffic(ports=[UPF_CORE_PORT]) 114 | print(f"Duration was {time.time() - s_time}") 115 | 116 | trex_stats = self.trex_client.get_stats() 117 | lat_stats = get_latency_stats(TREX_RECEIVER_PORT, trex_stats) 118 | flow_stats = get_flow_stats(TREX_RECEIVER_PORT, trex_stats) 119 | 120 | # Verify test results met baseline performance expectations 121 | 122 | # 0% packet loss 123 | self.assertEqual( 124 | flow_stats.tx_packets, 125 | flow_stats.rx_packets, 126 | f"Didn't receive all packets; sent {flow_stats.tx_packets}, received {flow_stats.rx_packets}", 127 | ) 128 | 129 | # 99.9th %ile latency < 1000 us 130 | self.assertLessEqual( 131 | lat_stats.percentile_99_9, 132 | 1000, 133 | f"99.9th %ile latency was higher than 1000 us! Was {lat_stats.percentile_99_9} us", 134 | ) 135 | 136 | # jitter < 20 us 137 | self.assertLessEqual( 138 | lat_stats.jitter, 139 | 20, 140 | f"Jitter was higher than 20 us! Was {lat_stats.jitter}", 141 | ) 142 | 143 | return 144 | -------------------------------------------------------------------------------- /ptf/tests/linerate/common.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2024 Intel Corporation 3 | 4 | from ipaddress import IPv4Address 5 | 6 | # MAC addresses 7 | TREX_SRC_MAC = "40:a6:b7:20:c8:25" # Source MAC address for DL traffic 8 | UPF_CORE_MAC = "40:a6:b7:20:4f:b9" # MAC address of N6 for the UPF/BESS 9 | UPF_ACCESS_MAC = "40:a6:b7:20:4f:b8" # MAC address of N3 for the UPF/BESS 10 | 11 | # Port setup 12 | TREX_SENDER_PORT = 1 13 | TREX_RECEIVER_PORT = 0 14 | UPF_CORE_PORT = 1 15 | UPF_ACCESS_PORT = 0 16 | 17 | # test specs 18 | DURATION = 10 19 | RATE = 100_000 # 100 Kpps 20 | UE_COUNT = 10_000 # 10k UEs 21 | PKT_SIZE = 64 22 | PKT_SIZE_L = 1400 # Packet size for MBR test 23 | 24 | # IP addresses 25 | UE_IP_START = IPv4Address("16.0.0.1") 26 | GNB_IP = IPv4Address("11.1.1.129") 27 | N3_IP = IPv4Address("198.18.0.1") 28 | PDN_IP = IPv4Address("6.6.6.6") # Must be routable by route_control 29 | 30 | -------------------------------------------------------------------------------- /ptf/tests/linerate/qos_metrics.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | import time 5 | from ipaddress import IPv4Address 6 | from pprint import pprint 7 | 8 | import ptf.testutils as testutils 9 | from grpc_test import * 10 | from pkt_utils import GTPU_PORT 11 | from trex_stl_lib.api import STLVM, STLPktBuilder, STLStream, STLTXCont 12 | from trex_test import TrexTest 13 | 14 | from common import * 15 | 16 | 17 | class PerFlowQosMetricsTest(TrexTest, GrpcTest): 18 | """ 19 | Generates 1 Mpps downlink traffic for 10k dest UE IP addresses. Uses 20 | BESS-UPF QoS metrics to verify baseline packet loss, latency, and jitter 21 | results. 22 | """ 23 | 24 | @autocleanup 25 | def runTest(self): 26 | n3TEID = 0 27 | endIP = UE_IP_START + UE_COUNT - 1 28 | 29 | # program UPF for downlink traffic by installing PDRs and FARs 30 | print("Installing PDRs and FARs...") 31 | for i in range(UE_COUNT): 32 | # install N6 DL PDR to match UE dst IP 33 | pdrDown = self.createPDR( 34 | srcIface=CORE, 35 | dstIP=int(UE_IP_START + i), 36 | srcIfaceMask=0xFF, 37 | dstIPMask=0xFFFFFFFF, 38 | precedence=255, 39 | fseID=n3TEID + i + 1, # start from 1 40 | ctrID=0, 41 | farID=i, 42 | qerIDList=[N6, 1], 43 | needDecap=0, 44 | ) 45 | self.addPDR(pdrDown) 46 | 47 | # install N6 DL FAR for encap 48 | farDown = self.createFAR( 49 | farID=i, 50 | fseID=n3TEID + i + 1, # start from 1 51 | applyAction=ACTION_FORWARD, 52 | dstIntf=DST_ACCESS, 53 | tunnelType=0x1, 54 | tunnelIP4Src=int(N3_IP), 55 | tunnelIP4Dst=int(GNB_IP), 56 | tunnelTEID=0, 57 | tunnelPort=GTPU_PORT, 58 | ) 59 | self.addFAR(farDown) 60 | 61 | # install N6 DL/UL application QER 62 | qer = self.createQER( 63 | gate=GATE_UNMETER, 64 | qerID=N6, 65 | fseID=n3TEID + i + 1, # start from 1 66 | qfi=9, 67 | ulGbr=0, 68 | ulMbr=0, 69 | dlGbr=0, 70 | dlMbr=0, 71 | burstDurationMs=10, 72 | ) 73 | self.addApplicationQER(qer) 74 | 75 | # set up trex to send traffic thru UPF 76 | print("Setting up TRex client...") 77 | vm = STLVM() 78 | vm.var( 79 | name="dst", 80 | min_value=str(UE_IP_START), 81 | max_value=str(endIP), 82 | size=4, 83 | op="random", 84 | ) 85 | vm.write(fv_name="dst", pkt_offset="IP.dst") 86 | vm.fix_chksum() 87 | 88 | pkt = testutils.simple_udp_packet( 89 | pktlen=PKT_SIZE, 90 | eth_dst=UPF_CORE_MAC, 91 | with_udp_chksum=False, 92 | ) 93 | stream = STLStream( 94 | packet=STLPktBuilder(pkt=pkt, vm=vm), 95 | mode=STLTXCont(pps=RATE), 96 | ) 97 | self.trex_client.add_streams(stream, ports=[UPF_CORE_PORT]) 98 | 99 | print("Running traffic...") 100 | s_time = time.time() 101 | self.trex_client.start(ports=[UPF_CORE_PORT], mult="1", duration=DURATION) 102 | 103 | # FIXME: pull QoS metrics at end instead of while traffic running 104 | time.sleep(DURATION) 105 | if self.trex_client.is_traffic_active(): 106 | stats = self.getSessionStats(q=[90, 99, 99.9], quiet=True) 107 | 108 | preQos = stats["preQos"] 109 | postDlQos = stats["postDlQos"] 110 | postUlQos = stats["postUlQos"] 111 | 112 | self.trex_client.wait_on_traffic(ports=[UPF_ACCESS_PORT]) 113 | print(f"Duration was {time.time() - s_time}") 114 | trex_stats = self.trex_client.get_stats() 115 | 116 | sent_packets = trex_stats["total"]["opackets"] 117 | recv_packets = trex_stats["total"]["ipackets"] 118 | 119 | # 0% packet loss 120 | self.assertEqual( 121 | sent_packets, 122 | recv_packets, 123 | f"Didn't receive all packets; sent {sent_packets}, received {recv_packets}", 124 | ) 125 | 126 | for fseid in postDlQos: 127 | lat = fseid["latency"]["percentileValuesNs"] 128 | jitter = fseid["jitter"]["percentileValuesNs"] 129 | 130 | # 99th %ile latency < 100 us 131 | self.assertLessEqual( 132 | int(lat[1]) / 1000, 133 | 100, 134 | f"99th %ile latency was higher than 100 us! Was {int(lat[1]) / 1000} us", 135 | ) 136 | 137 | # 99.9th %ile latency < 200 us 138 | self.assertLessEqual( 139 | int(lat[2]) / 1000, 140 | 200, 141 | f"99.9th %ile latency was higher than 200 us! Was {int(lat[2]) / 1000} us", 142 | ) 143 | 144 | # 99th% jitter < 100 us 145 | self.assertLessEqual( 146 | int(jitter[1]) / 1000, 147 | 100, 148 | f"99th %ile jitter was higher than 100 us! Was {int(jitter[1]) / 1000} us", 149 | ) 150 | 151 | return 152 | -------------------------------------------------------------------------------- /ptf/tests/unary/check_rules.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2021 Open Networking Foundation 3 | 4 | from ipaddress import IPv4Address 5 | 6 | from grpc_test import * 7 | from trex_test import TrexTest 8 | 9 | UPF_DEST_MAC = "0c:c4:7a:19:6d:ca" 10 | 11 | # Port setup 12 | TREX_SENDER_PORT = 0 13 | TREX_RECEIVER_PORT = 1 14 | BESS_SENDER_PORT = 2 15 | BESS_RECEIVER_PORT = 3 16 | 17 | 18 | class PdrTest(TrexTest, GrpcTest): 19 | def runTest(self): 20 | # create basic N6 downlink pdr 21 | pdr = self.createPDR( 22 | srcIface=CORE, 23 | dstIP=int(IPv4Address("16.0.0.1")), 24 | srcIfaceMask=0xFF, 25 | dstIPMask=0xFFFFFFFF, 26 | precedence=255, 27 | fseID=0x30000000, 28 | ctrID=0, 29 | farID=N3, 30 | qerIDList=[N6, 1], 31 | needDecap=0, 32 | ) 33 | 34 | print("add pdr response:") 35 | self.addPDR(pdr, debug=True) 36 | print() 37 | 38 | # Testing purposes: verify bess fails to find PDR when modified 39 | # pdr = pdr._replace(srcIfaceMask=0xAF) 40 | print("del pdr response:") 41 | self.delPDR(pdr, debug=True) 42 | print() 43 | 44 | 45 | class FarTest(TrexTest, GrpcTest): 46 | def runTest(self): 47 | # create basic N6 uplink FAR 48 | far = self.createFAR( 49 | farID=N6, 50 | fseID=0x30000000, 51 | applyAction=ACTION_FORWARD, 52 | dstIntf=DST_CORE, 53 | ) 54 | 55 | print("add far response:") 56 | self.addFAR(far, debug=True) 57 | print() 58 | 59 | # Testing purposes: verify bess fails to find FAR when modified 60 | # far = far._replace(fseID=0xA0000000) 61 | print("del far response:") 62 | self.delFAR(far, debug=True) 63 | print() 64 | 65 | 66 | class QerAppTest(TrexTest, GrpcTest): 67 | def runTest(self): 68 | # configure as basic N6 UL/DL QER 69 | qer = self.createQER( 70 | gate=GATE_UNMETER, 71 | qfi=9, 72 | qerID=N6, 73 | fseID=0x30000000, 74 | ulGbr=0, 75 | ulMbr=0, 76 | dlGbr=0, 77 | dlMbr=0, 78 | burstDurationMs=100, 79 | ) 80 | 81 | print("add qer response:") 82 | self.addApplicationQER(qer, debug=True) 83 | print() 84 | 85 | print("del qer response:") 86 | self.delApplicationQER(qer, debug=True) 87 | print() 88 | 89 | 90 | class QerSessionTest(TrexTest, GrpcTest): 91 | def runTest(self): 92 | # configure as basic N6 UL/DL QER 93 | qer = self.createQER( 94 | gate=GATE_UNMETER, 95 | qfi=0, 96 | qerID=1, 97 | fseID=0x30000000, 98 | ulGbr=0, 99 | ulMbr=0, 100 | dlGbr=0, 101 | dlMbr=0, 102 | burstDurationMs=100, 103 | ) 104 | 105 | print("add qer response:") 106 | self.addSessionQER(qer, debug=True) 107 | print() 108 | 109 | print("del qer response:") 110 | self.delSessionQER(qer, debug=True) 111 | print() 112 | 113 | 114 | class MetricsTest(GrpcTest): 115 | @autocleanup 116 | def runTest(self): 117 | print(self.getPortStats("core")) 118 | self.getSessionStats() 119 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright 2023 Intel Corporation 3 | 4 | flask 5 | grpcio 6 | iptools 7 | jsoncomment 8 | mitogen 9 | protobuf==3.20.3 10 | psutil 11 | pyroute2 12 | scapy 13 | -------------------------------------------------------------------------------- /scripts/install_ntf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright 2020 Intel Corporation 4 | 5 | ENABLE_NTF=${ENABLE_NTF:-0} 6 | NTF_COMMIT=${NTF_COMMIT:-master} 7 | CJOSE_COMMIT=${CJOSE_COMMIT:-9261231f08d2a3cbcf5d73c5f9e754a2f1c379ac} 8 | PLUGINS_DIR=${PLUGINS_DIR:-"plugins"} 9 | PLUGIN_DEST_DIR="${PLUGINS_DIR}/ntf" 10 | 11 | SUDO='' 12 | [[ $EUID -ne 0 ]] && SUDO=sudo 13 | 14 | install_ntf() { 15 | $SUDO apt install -y \ 16 | autoconf \ 17 | automake \ 18 | clang-format \ 19 | doxygen \ 20 | libjansson-dev \ 21 | libjansson4 \ 22 | libtool 23 | 24 | mkdir /tmp/cjose 25 | pushd /tmp/cjose 26 | curl -L "https://github.com/cisco/cjose/tarball/${CJOSE_COMMIT}" | 27 | tar xz -C . --strip-components=1 28 | ./configure --prefix=/usr 29 | make 30 | make install 31 | popd 32 | 33 | mkdir ${PLUGIN_DEST_DIR} 34 | pushd ${PLUGIN_DEST_DIR} 35 | curl -L "https://github.com/Network-Tokens/ntf/tarball/${NTF_COMMIT}" | 36 | tar xz -C . --strip-components=1 37 | popd 38 | } 39 | 40 | cleanup_image() { 41 | $SUDO rm -rf /var/lib/apt/lists/* 42 | $SUDO apt clean 43 | } 44 | 45 | (return 2>/dev/null) && echo "Sourced" && return 46 | 47 | set -o errexit 48 | set -o pipefail 49 | set -o nounset 50 | 51 | [ "$ENABLE_NTF" == "0" ] && exit 0 52 | 53 | echo "Installing NTF plugin..." 54 | install_ntf 55 | 56 | echo "Cleaning up..." 57 | cleanup_image 58 | -------------------------------------------------------------------------------- /scripts/reset_upf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright 2020 Intel Corporation 4 | # 5 | # Usage: reset_upf.sh cndp|dpdk true|false 6 | # Currently this script only supports CNDP and DPDK modes. 7 | 8 | MODE=${1:-cndp} 9 | 10 | BUSY_POLL=${2:-true} 11 | 12 | ACCESS_PCIE=0000:86:00.0 13 | CORE_PCIE=0000:88:00.0 14 | 15 | ACCESS_IFACE=enp134s0 16 | CORE_IFACE=enp136s0 17 | 18 | SET_IRQ_AFFINITY=~/nic/driver/ice-1.9.11/scripts/set_irq_affinity 19 | 20 | sudo dpdk-devbind.py -u $ACCESS_PCIE --force 21 | sudo dpdk-devbind.py -u $CORE_PCIE --force 22 | 23 | sleep 2 24 | echo "Stop UPF docker containers" 25 | docker stop pause bess bess-routectl bess-web bess-pfcpiface || true 26 | docker rm -f pause bess bess-routectl bess-web bess-pfcpiface || true 27 | 28 | if [ "$MODE" == 'cndp' ]; then 29 | echo "Bind access/core interface to ICE driver" 30 | sudo dpdk-devbind.py -b ice $ACCESS_PCIE 31 | sudo dpdk-devbind.py -b ice $CORE_PCIE 32 | sudo ifconfig $ACCESS_IFACE up 33 | sudo ifconfig $CORE_IFACE up 34 | sudo systemctl disable --now irqbalance 35 | sudo $SET_IRQ_AFFINITY all $ACCESS_IFACE $CORE_IFACE 36 | 37 | else 38 | echo "Bind access/core interface to DPDK" 39 | sudo dpdk-devbind.py -b vfio-pci $ACCESS_PCIE 40 | sudo dpdk-devbind.py -b vfio-pci $CORE_PCIE 41 | fi 42 | 43 | sleep 2 44 | 45 | if [[ "$MODE" == 'cndp' ]] && [[ "$BUSY_POLL" == 'true' ]]; then 46 | echo "Setup configuration for XDP socket Busy Polling" 47 | 48 | # Refer: https://lwn.net/Articles/836250/ 49 | echo 10 | sudo tee /sys/class/net/$ACCESS_IFACE/napi_defer_hard_irqs 50 | echo 2000000 | sudo tee /sys/class/net/$ACCESS_IFACE/gro_flush_timeout 51 | 52 | echo 10 | sudo tee /sys/class/net/$CORE_IFACE/napi_defer_hard_irqs 53 | echo 2000000 | sudo tee /sys/class/net/$CORE_IFACE/gro_flush_timeout 54 | fi 55 | -------------------------------------------------------------------------------- /scripts/telemetry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright 2021 Intel Corporation 4 | 5 | # SPDX-License-Identifier: Apache-2.0 6 | # Copyright 2021 Open Networking Foundation 7 | 8 | docker rm -f prom grafana || true 9 | 10 | docker run -d --name prom \ 11 | --net=host \ 12 | -v $PWD/conf/prometheus.yml:/etc/prometheus/prometheus.yml \ 13 | prom/prometheus 14 | 15 | docker run -d --name grafana \ 16 | --net=host \ 17 | -v $PWD/conf/grafana:/etc/grafana/provisioning \ 18 | -v $PWD/conf/grafana/:/var/lib/grafana/dashboards/ \ 19 | grafana/grafana 20 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | 5 | # E2E integration tests 6 | 7 | The tests defined in this directory implement the so-called "broad integration tests" 8 | (they are sometimes called system tests or E2E tests, see [Martin Fowler's blog](https://martinfowler.com/bliki/IntegrationTest.html)). 9 | 10 | The purpose of E2E integration tests is to verify the behavior of the PFCP Agent with different flavors of PFCP messages, 11 | as well as to check PFCP Agent's integration with data plane components (UP4, BESS-UPF). In detail, these tests verify if 12 | PFCP messages are handled as expected by the PFCP Agent, and if the PFCP Agent installs correct packet forwarding rules onto the fast-path target (UP4/BESS). 13 | 14 | ## Structure 15 | 16 | - `infra/`: contains build and deployment files. 17 | - `config/`: contains app-specific config (e.g., `upf.jsonc`). 18 | - the current directory contains `*_test.go` files defining test scenarios. 19 | 20 | ## Overview 21 | 22 | The E2E integration tests are integrated within the Go test framework and can be run by `go test`. 23 | 24 | The tests use `docker-compose` to set up `pfcpiface` and `mock-up4` (the BMv2 container running the UP4 pipeline) images. 25 | Then, a given test case generates PFCP messages towards `pfcpiface` and fetches the runtime forwarding configuration from the 26 | data plane component (e.g., via P4Runtime for UP4) to verify forwarding state configuration. 27 | 28 | ## Run tests 29 | 30 | To run all E2E integration tests invoke the command below from the root directory: 31 | 32 | ```bash 33 | make test-up4-integration 34 | ``` -------------------------------------------------------------------------------- /test/integration/conf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package integration 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "net/http" 11 | "os" 12 | "runtime" 13 | "time" 14 | 15 | "github.com/omec-project/upf-epc/pfcpiface" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | const ( 20 | ConfigDefault = iota 21 | ConfigUPFBasedIPAllocation 22 | ConfigWipeOutOnUP4Restart 23 | ) 24 | 25 | const ( 26 | UEPoolUPF = "10.250.0.0/16" 27 | UEPoolCP = "17.0.0.0/16" 28 | ) 29 | 30 | var baseConfig = pfcpiface.Conf{ 31 | ReadTimeout: 15, 32 | RespTimeout: "2s", 33 | LogLevel: zap.InfoLevel, 34 | } 35 | 36 | func BESSConfigDefault() pfcpiface.Conf { 37 | var intf string 38 | 39 | switch runtime.GOOS { 40 | case "darwin": 41 | intf = "lo0" 42 | case "linux": 43 | intf = "lo" 44 | } 45 | 46 | config := baseConfig 47 | config.AccessIface = pfcpiface.IfaceType{ 48 | IfName: intf, 49 | } 50 | config.CoreIface = pfcpiface.IfaceType{ 51 | IfName: intf, 52 | } 53 | return config 54 | } 55 | 56 | func BESSConfigUPFBasedIPAllocation() pfcpiface.Conf { 57 | config := BESSConfigDefault() 58 | config.CPIface = pfcpiface.CPIfaceInfo{ 59 | EnableUeIPAlloc: true, 60 | UEIPPool: UEPoolUPF, 61 | } 62 | 63 | return config 64 | } 65 | 66 | func UP4ConfigDefault() pfcpiface.Conf { 67 | var up4Server string 68 | switch os.Getenv(EnvMode) { 69 | case ModeDocker: 70 | up4Server = "mock-up4" 71 | case ModeNative: 72 | up4Server = "127.0.0.1" 73 | } 74 | 75 | config := baseConfig 76 | config.EnableP4rt = true 77 | config.EnableGtpuPathMonitoring = false 78 | config.P4rtcIface = pfcpiface.P4rtcInfo{ 79 | SliceID: 1, 80 | AccessIP: upfN3Address + "/32", 81 | P4rtcServer: up4Server, 82 | P4rtcPort: "50001", 83 | QFIToTC: map[uint8]uint8{ 84 | 8: 2, 85 | }, 86 | DefaultTC: 3, 87 | } 88 | 89 | config.CPIface = pfcpiface.CPIfaceInfo{ 90 | UEIPPool: UEPoolCP, 91 | } 92 | 93 | return config 94 | } 95 | 96 | func UP4ConfigUPFBasedIPAllocation() pfcpiface.Conf { 97 | config := UP4ConfigDefault() 98 | config.CPIface = pfcpiface.CPIfaceInfo{ 99 | EnableUeIPAlloc: true, 100 | UEIPPool: UEPoolUPF, 101 | } 102 | 103 | return config 104 | } 105 | 106 | func UP4ConfigWipeOutOnUP4Restart() pfcpiface.Conf { 107 | config := UP4ConfigDefault() 108 | config.P4rtcIface.ClearStateOnRestart = true 109 | 110 | return config 111 | } 112 | 113 | func GetConfig(datapath string, configType uint32) pfcpiface.Conf { 114 | switch datapath { 115 | case DatapathUP4: 116 | switch configType { 117 | case ConfigDefault: 118 | return UP4ConfigDefault() 119 | case ConfigUPFBasedIPAllocation: 120 | return UP4ConfigUPFBasedIPAllocation() 121 | case ConfigWipeOutOnUP4Restart: 122 | return UP4ConfigWipeOutOnUP4Restart() 123 | } 124 | case DatapathBESS: 125 | switch configType { 126 | case ConfigDefault: 127 | return BESSConfigDefault() 128 | case ConfigUPFBasedIPAllocation: 129 | return BESSConfigUPFBasedIPAllocation() 130 | } 131 | } 132 | 133 | panic("wrong datapath or config type provided") 134 | } 135 | 136 | func PushSliceMeterConfig(sliceConfig pfcpiface.NetworkSlice) error { 137 | rawSliceConfig, err := json.Marshal(sliceConfig) 138 | if err != nil { 139 | return err 140 | } 141 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 142 | defer cancel() 143 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.8:8080/v1/config/network-slices", bytes.NewBuffer(rawSliceConfig)) 144 | if err != nil { 145 | return err 146 | } 147 | resp, err := http.DefaultClient.Do(req) 148 | req.Header.Set("Content-Type", "application/json") 149 | if err != nil { 150 | return err 151 | } 152 | defer resp.Body.Close() 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /test/integration/providers/p4runtime.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package providers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/antoninbas/p4runtime-go-client/pkg/client" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | 15 | p4_v1 "github.com/p4lang/p4runtime/go/p4/v1" 16 | ) 17 | 18 | var ( 19 | stopCh chan struct{} 20 | grpcConn *grpc.ClientConn 21 | ) 22 | 23 | func TimeBasedElectionId() p4_v1.Uint128 { 24 | now := time.Now() 25 | return p4_v1.Uint128{ 26 | High: uint64(now.Unix()), 27 | Low: uint64(now.UnixNano() % 1e9), 28 | } 29 | } 30 | 31 | func ConnectP4rt(addr string, asMaster bool) (*client.Client, error) { 32 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 33 | defer cancel() 34 | 35 | var err error 36 | 37 | grpcConn, err = grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | c := p4_v1.NewP4RuntimeClient(grpcConn) 43 | // Election only happens if asMaster is true. 44 | p4RtC := client.NewClient(c, 1, TimeBasedElectionId(), client.DisableCanonicalBytestrings) 45 | 46 | if asMaster { 47 | // perform Master Arbitration 48 | stopCh = make(chan struct{}) 49 | arbitrationCh := make(chan bool) 50 | go p4RtC.Run(stopCh, arbitrationCh, nil) 51 | 52 | timeout := 5 * time.Second 53 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 54 | defer cancel() 55 | select { 56 | case <-ctx.Done(): 57 | return nil, fmt.Errorf("failed to connect to P4Runtime server") 58 | case <-arbitrationCh: 59 | } 60 | } else { 61 | // deletes channel, otherwise DisconnectP4rt blocks forever for non-master P4runtime channel 62 | stopCh = nil 63 | } 64 | 65 | // used to retrieve P4Info if exists on device 66 | p4RtC.GetFwdPipe(client.GetFwdPipeP4InfoAndCookie) 67 | 68 | return p4RtC, nil 69 | } 70 | 71 | func DisconnectP4rt() { 72 | if stopCh != nil { 73 | stopCh <- struct{}{} 74 | } 75 | // wait for P4rt stream to be closed 76 | // FIXME: p4runtime-go-client fatals if gRPC channel is closed before P4rt stream is terminated. 77 | // The lib doesn't give a better way to wait for stream to be terminated. 78 | time.Sleep(1 * time.Second) 79 | if grpcConn != nil { 80 | grpcConn.Close() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/integration/verify_bess.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2022 Open Networking Foundation 3 | 4 | package integration 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/omec-project/upf-epc/pkg/fake_bess" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // TODO: current assertions are limited to quantity verification only. We'd like to extend this 14 | // and check entry contents as well. 15 | func verifyBessEntries(t *testing.T, bess *fake_bess.FakeBESS, expectedValues p4RtValues) { 16 | // Check we have all expected PDRs. 17 | pdrs := bess.GetPdrTableEntries() 18 | require.Equal(t, len(expectedValues.pdrs), len(pdrs), "found unexpected PDR entries %v", pdrs) 19 | for _, expectedPdr := range expectedValues.pdrs { 20 | id, err := expectedPdr.PDRID() 21 | require.NoError(t, err) 22 | 23 | _, found := pdrs[uint32(id)] 24 | require.True(t, found, "missing PDR", "expected PDR with ID %v: %+v, got %+v", id, expectedPdr, pdrs) 25 | } 26 | 27 | // Check we have all expected FARs. 28 | fars := bess.GetFarTableEntries() 29 | require.Equal(t, len(expectedValues.pdrs), len(fars), "found unexpected FAR entries %v", fars) 30 | for _, expectedFar := range expectedValues.fars { 31 | id, err := expectedFar.FARID() 32 | require.NoError(t, err) 33 | 34 | _, found := fars[id] 35 | require.True(t, found, "missing FAR", "expected FAR with ID %v: %+v, got %+v", id, expectedFar, fars) 36 | } 37 | 38 | // Check we have all expected session and app QERs. 39 | qers := append(bess.GetSessionQerTableEntries(), bess.GetAppQerTableEntries()...) 40 | require.Equal(t, len(expectedValues.qers)*2 /* up and down link */, len(qers), "found unexpected QER entries %v", qers) 41 | } 42 | 43 | func verifyNoBessRuntimeEntries(t *testing.T, bess *fake_bess.FakeBESS) { 44 | pdrs := bess.GetPdrTableEntries() 45 | require.Equal(t, 0, len(pdrs), "found unexpected PDR entries: %v", pdrs) 46 | fars := bess.GetFarTableEntries() 47 | require.Equal(t, 0, len(fars), "found unexpected FAR entries: %v", fars) 48 | sessionQers := bess.GetSessionQerTableEntries() 49 | require.Equal(t, 0, len(sessionQers), "found unexpected session QER entries: %v", sessionQers) 50 | appQers := bess.GetAppQerTableEntries() 51 | require.Equal(t, 0, len(appQers), "found unexpected app QER entries: %v", appQers) 52 | } 53 | --------------------------------------------------------------------------------