Repository: l3af-project/l3afd Branch: main Commit: 4ce859684188 Files: 80 Total size: 472.1 KB Directory structure: gitextract_j3_x9dxv/ ├── .github/ │ ├── dependabot.yml │ ├── release.yml │ └── workflows/ │ ├── ci-build-windows.yaml │ ├── ci-build.yaml │ ├── ci-e2e.yaml │ ├── codeql.yaml │ └── scorecards-analysis.yml ├── .gitignore ├── CMakeLists.txt ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── apis/ │ ├── configwatch.go │ ├── configwatch_test.go │ ├── handlers/ │ │ ├── addprog.go │ │ ├── addprog_test.go │ │ ├── deleteprog.go │ │ ├── deleteprog_test.go │ │ ├── getconfig.go │ │ ├── getconfig_test.go │ │ ├── restart_linux.go │ │ ├── restart_linux_test.go │ │ ├── restart_windows.go │ │ ├── restart_windows_test.go │ │ ├── updateconfig.go │ │ └── updateconfig_test.go │ └── routes.go ├── bpfprogs/ │ ├── bpf.go │ ├── bpfCfgs_internal.go │ ├── bpf_test.go │ ├── bpf_test_unix.go │ ├── bpf_test_windows.go │ ├── bpf_unix.go │ ├── bpf_windows.go │ ├── bpfdebug.go │ ├── bpfmap.go │ ├── bpfmap_test.go │ ├── bpfmetrics.go │ ├── bpfmetrics_test.go │ ├── nfconfig.go │ ├── nfconfig_test.go │ ├── probes.go │ ├── probes_test.go │ ├── processCheck.go │ └── processCheck_test.go ├── build-docker/ │ ├── Dockerfile │ └── start.sh ├── config/ │ ├── config.go │ ├── config_loader.go │ └── l3afd.cfg ├── config.yml ├── docs/ │ ├── CONTRIBUTING.md │ ├── api/ │ │ └── README.md │ ├── configdoc.md │ ├── docs.go │ ├── graceful-restart-guide.md │ ├── prod-deploy-guide.md │ ├── swagger.json │ ├── swagger.md │ └── swagger.yaml ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── mocks/ │ └── mocked_interfaces.go ├── models/ │ └── l3afd.go ├── pidfile/ │ └── pidfile.go ├── register_internal.go ├── restart/ │ ├── restart.go │ └── restart_test.go ├── routes/ │ ├── route.go │ └── router.go ├── signals/ │ ├── signal_unix.go │ └── signal_windows.go ├── stats/ │ └── metrics.go ├── testdata/ │ ├── Test_l3af-config.json │ └── l3afd.cdb ├── utils/ │ └── utils.go └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the format of this file, see # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" # Workflow files are stored in the default location of `.github/workflows` directory: "/" schedule: interval: "weekly" day: "saturday" - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" day: "saturday" ================================================ FILE: .github/release.yml ================================================ # .github/release.yml --- changelog: exclude: labels: - ignore-for-release categories: - title: Breaking Changes 💥 labels: - breaking-change - breaking - title: New Features 🎉 labels: - feat - enhancement - title: Bug Fixes 🐛 labels: - fix - bugfix - bug - title: Other Changes labels: - "*" ================================================ FILE: .github/workflows/ci-build-windows.yaml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the github environment, see # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # # For documentation on the syntax of this file, see # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: CI Windows build on: pull_request: {} push: branches: - main permissions: contents: read jobs: build: runs-on: windows-latest steps: - name: Setup Go 1.26.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-version: '1.26.0' - name: Harden Runner uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Set up git env run: | git config --global core.autocrlf false $gopath = (go env GOPATH) echo "GOPATH=$gopath" >> $env:GITHUB_ENV - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Format run: | go install golang.org/x/tools/cmd/goimports@latest $goimp = (Join-path -Path (go env GOPATH) -ChildPath "\bin\goimports") $res = (&$goimp -l .) -replace "$_" if ($res -ne "") { echo "Unformatted source code:" echo $res exit 1 } - name: Vet run: | go vet -tags WINDOWS ./... - name: Test run: | go test -tags WINDOWS ./... - uses: dominikh/staticcheck-action@9716614d4101e79b4340dd97b10e54d68234e431 with: version: "2025.1.1" install-go: false cache-key: "1.24.x" build-tags: WINDOWS - name: Build env: GOPATH: ${{env.GOPATH}} run: | cmake -B build cmake --build build ================================================ FILE: .github/workflows/ci-build.yaml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the github environment, see # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # # For documentation on the syntax of this file, see # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: CI Ubuntu build on: pull_request: {} push: branches: - main permissions: contents: read jobs: build: strategy: matrix: os: - ubuntu-24.04 - ubuntu-22.04 runs-on: ${{ matrix.os }} steps: - name: Setup Go 1.26.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-version: '1.26.0' - name: Harden Runner uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Set up environment run: | sudo apt-get update sudo apt-get remove -y containerd.io docker docker.io moby-engine moby-cli || true # Remove any existing Docker-related packages sudo apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ software-properties-common # docker src curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io sudo apt-get install -y gcc libc-dev bash perl curl make - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Format run: | go install golang.org/x/tools/cmd/goimports@latest res="$(goimports -l .)" if [[ "$(printf '%s' "$res")" != '' ]]; then echo "Unformatted source code:" echo "$res" exit 1 fi - name: Vet run: | go vet ./... - name: Test run: | go test ./... go clean -modcache - uses: dominikh/staticcheck-action@9716614d4101e79b4340dd97b10e54d68234e431 with: version: "2025.1.1" install-go: false cache-key: "1.24.x" - name: Build run: | make - name: Copy files if: github.ref == 'refs/heads/main' run: | sudo cp ./config/l3afd.cfg ./build-docker sudo cp l3afd ./build-docker - name: login to docker registry if: github.ref == 'refs/heads/main' uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_TOKEN}} - name: build and push docker image to registry if: github.ref == 'refs/heads/main' uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: context: ./build-docker push: true tags: linuxfoundationl3af/l3afd:latest - name: upload l3afd binary if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: l3afd-latest-linux-x86_64-${{ matrix.os }} path: l3afd ================================================ FILE: .github/workflows/ci-e2e.yaml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the github environment, see # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # # For documentation on the syntax of this file, see # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: CI E2E build on: pull_request: {} push: branches: - main permissions: contents: read jobs: build: strategy: matrix: os: - ubuntu-24.04 - ubuntu-22.04 runs-on: ${{ matrix.os }} steps: - name: Update and firewall stop run: | sudo apt update sudo systemctl stop ufw sudo apt install -y iproute2 sudo apt install git curl hey - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Prep run: | sudo cp -r /home/runner/work/l3afd/l3afd /root sudo git clone https://github.com/l3af-project/l3af-arch.git /root/l3af-arch sudo bash /root/l3af-arch/dev_environment/e2e_test/prep_env.sh sudo bash /root/l3af-arch/dev_environment/setup_linux_dev_env.sh --ci-build hm=$(hostname) sudo find /root/l3af-arch/dev_environment/e2e_test -type f -name "*.json" -exec sed -i "s/l3af-test-host/$hm/g" {} + - name: Run Tests run: | sudo bash /root/l3af-arch/dev_environment/e2e_test/test_suite.sh ================================================ FILE: .github/workflows/codeql.yaml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the github environment, see # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # # For documentation on the syntax of this file, see # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: "CodeQL" on: push: branches: [ main ] pull_request: branches: [ main ] # Declare default permissions as read only. permissions: read-all jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] steps: - name: Harden Runner uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df with: egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Initialize CodeQL uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 ================================================ FILE: .github/workflows/scorecards-analysis.yml ================================================ # Copyright Contributors to the L3AF Project. # SPDX-License-Identifier: Apache-2.0 # # For documentation on the github environment, see # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners # # For documentation on the syntax of this file, see # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: Scorecards on: push: branches: [ main ] pull_request: branches: [ main ] concurrency: # Cancel any Scorecards workflow currently in progress for the same PR. # Allow running concurrently with any other commits. group: scorecards-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecards analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write id-token: write actions: read contents: read steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: ${{ github.event_name != 'pull_request' }} # Upload the results as artifacts (optional). - name: "Upload artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard so it will be visible # at https://github.com/l3af-project/l3afd/security/code-scanning. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 with: sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ .idea/ vendor/ ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(l3afd) add_custom_target(swagger ALL DEPENDS ${CMAKE_SOURCE_DIR}/docs/docs.go ${CMAKE_SOURCE_DIR}/docs/swagger.json ${CMAKE_SOURCE_DIR}/docs/swagger.yaml) add_custom_command(OUTPUT $ENV{GOPATH}/bin/swag.exe COMMAND go install github.com/swaggo/swag/cmd/swag@latest COMMAND go get -u github.com/swaggo/http-swagger COMMAND go get -u github.com/alecthomas/template) add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/docs/docs.go ${CMAKE_SOURCE_DIR}/docs/swagger.json ${CMAKE_SOURCE_DIR}/docs/swagger.yaml DEPENDS ${CMAKE_SOURCE_DIR}/apis/configwatch.go $ENV{GOPATH}/bin/swag.exe WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND "$ENV{GOPATH}/bin/swag.exe" init -d "./" -g "apis/configwatch.go") add_custom_target(build ALL DEPENDS ${CMAKE_SOURCE_DIR}/l3afd.exe) if (${WIN32}) add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/l3afd.exe DEPENDS ${CMAKE_SOURCE_DIR}/main.go WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND go build -tags WINDOWS .) else () add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/l3afd.exe DEPENDS ${CMAKE_SOURCE_DIR}/main.go WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND go build .) endif () ================================================ FILE: CODEOWNERS ================================================ # Default Code Owners * @sanfern @charleskbliu0 @jniesz @dalalkaran @pmoroney ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ .PHONY: all export GOPATH := $(HOME)/go all: swagger build swagger: @mkdir $(GOPATH) || true @go install github.com/swaggo/swag/cmd/swag@latest @$(GOPATH)/bin/swag init -d "./" -g "apis/configwatch.go" build: @CGO_ENABLED=0 go build -ldflags \ "-X main.Version=v2.1.0 \ -X main.VersionSHA=`git rev-parse HEAD`" install: swagger @go mod tidy @CGO_ENABLED=0 go install -ldflags \ "-X main.Version=v2.1.0 \ -X main.VersionSHA=`git rev-parse HEAD`" cibuild: swagger @go mod tidy @CGO_ENABLED=0 go install -cover -ldflags \ "-X main.Version=v2.1.0 \ -X main.VersionSHA=`git rev-parse HEAD`" ================================================ FILE: README.md ================================================ # L3AFD: Lightweight eBPF Daemon ![L3AF_Logo](https://github.com/l3af-project/l3af-arch/blob/main/images/logos/Color/L3AF_logo.svg) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6075/badge)](https://bestpractices.coreinfrastructure.org/projects/6075) [![Go Report Card](https://goreportcard.com/badge/github.com/l3af-project/l3afd)](https://goreportcard.com/report/github.com/l3af-project/l3afd) [![GoDoc](https://godoc.org/github.com/l3af-project/l3afd?status.svg)](https://pkg.go.dev/github.com/l3af-project/l3afd) [![Apache licensed](https://img.shields.io/badge/license-Apache-blue.svg)](LICENSE) [![L3AF Slack](https://img.shields.io/badge/slack-L3AF-brightgreen.svg?logo=slack)](http://l3afworkspace.slack.com/) L3AFD is a crucial part of the L3AF ecosystem. For more information on L3AF see https://l3af.io/ # Overview L3AFD is the primary component of the L3AF control plane. L3AFD is a daemon that orchestrates and manages multiple eBPF programs. L3AFD runs on each node where the user wishes to run eBPF programs. L3AFD reads configuration data and manages the execution and monitoring of eBPF programs running on the node. L3AFD downloads pre-built eBPF programs from a user-configured repository. However, we envision the creation of a community-driven eBPF package marketplace where L3AF users can obtain a variety of eBPF programs developed by multiple sources. ![L3AF Platform](https://github.com/l3af-project/l3af-arch/blob/main/images/L3AF_platform.png) # Try it out See our [L3AF Development Environment](https://github.com/l3af-project/l3af-arch/tree/main/dev_environment) for a quick and easy way to try out L3AF on your local machine. # Installing Try [a binary release](https://github.com/l3af-project/l3afd/releases/latest). # Building To build on your local machine, including swagger docs do the following. For Linux: ``` make ``` For Windows: ``` cmake -B build cmake --build build ``` # Docker build - L3AFD binary & configuration that is required in the Docker image needs to be built locally and copied to build-docker directory - Execute below command to build the docker image ``` docker build -t l3afd: -f Dockerfile . ``` Requirements to run L3AFD as a Container - BPF, debugfs & shared-memory filesystems mount points should be available in the container - L3AFD container needs privileged access as it needs to manage eBPF programs - eBPF programs should be attached to the host interface so that it will apply to all the containers in the host In order to satisfy the above requirements L3afd docker container needs to be run using the below command ``` docker run -d -v /sys/fs/bpf:/sys/fs/bpf -v /sys/kernel/debug/:/sys/kernel/debug/ -v /dev/shm:/dev/shm --privileged --net=host l3afd: ``` # Testing To test on your local machine, do the following. For Linux: ``` go test ./... ``` For Windows: ``` go test -tags WINDOWS ./... ``` # Generate Swagger Docs See our [Swaggo setup](docs/swagger.md) # Contributing Contributing to L3afd is fun. To get started: - [Contributing guide](docs/CONTRIBUTING.md) ================================================ FILE: apis/configwatch.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !configs // +build !configs package apis import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "net" "net/http" "os" "os/signal" "path" "regexp" "strings" "time" "unicode/utf8" httpSwagger "github.com/swaggo/http-swagger" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/routes" "github.com/l3af-project/l3afd/v2/signals" _ "github.com/l3af-project/l3afd/v2/docs" "github.com/rs/zerolog/log" ) type Server struct { BPFRTConfigs *bpfprogs.NFConfigs HostName string l3afdServer *http.Server CaCertPool *x509.CertPool SANMatchRules []string } // @title L3AFD APIs // @version 1.0 // @description Configuration APIs to deploy and get the details of the eBPF Programs on the node // @host // @BasePath / func StartConfigWatcher(ctx context.Context, hostname, daemonName string, conf *config.Config, bpfrtconfg *bpfprogs.NFConfigs) error { log.Info().Msgf("%s config server setup started on host %s", daemonName, hostname) s := &Server{ BPFRTConfigs: bpfrtconfg, HostName: hostname, l3afdServer: &http.Server{ Addr: conf.L3afConfigsRestAPIAddr, }, SANMatchRules: conf.MTLSSANMatchRules, } if _, ok := models.AllNetListeners.Load("main_http"); !ok { tcpAddr, err := net.ResolveTCPAddr("tcp", conf.L3afConfigsRestAPIAddr) if err != nil { return fmt.Errorf("error resolving TCP address:%w", err) } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { return fmt.Errorf("creating tcp listner failed with %w", err) } models.AllNetListeners.Store("main_http", listener) } term := make(chan os.Signal, 1) signal.Notify(term, signals.ShutdownSignals...) go func() { <-term s.GracefulStop(conf.ShutdownTimeout) ctx.Done() log.Info().Msg("L3afd gracefulStop completed") }() go func() { r := routes.NewRouter(apiRoutes(ctx, bpfrtconfg)) if conf.SwaggerApiEnabled { r.Mount("/swagger", httpSwagger.WrapHandler) } s.l3afdServer.Handler = r // As per design discussion when mTLS flag is not set and not listening on loopback or localhost if !conf.MTLSEnabled && !isLoopback(conf.L3afConfigsRestAPIAddr) && conf.Environment == config.ENV_PROD { conf.MTLSEnabled = true } val, _ := models.AllNetListeners.Load("main_http") l, _ := val.(*net.TCPListener) if conf.MTLSEnabled { log.Info().Msgf("l3afd server listening with mTLS - %s ", conf.L3afConfigsRestAPIAddr) // Create a CA certificate pool and add client ca's to it caCert, err := os.ReadFile(path.Join(conf.MTLSCertDir, conf.MTLSCACertFilename)) if err != nil { log.Fatal().Err(err).Msgf("client CA %s file not found", conf.MTLSCACertFilename) } s.CaCertPool, _ = x509.SystemCertPool() if s.CaCertPool == nil { s.CaCertPool = x509.NewCertPool() } if ok := s.CaCertPool.AppendCertsFromPEM(caCert); !ok { log.Warn().Msgf("No client certs appended for mTLS") } serverCertFile := path.Join(conf.MTLSCertDir, conf.MTLSServerCertFilename) serverKeyFile := path.Join(conf.MTLSCertDir, conf.MTLSServerKeyFilename) serverCert, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile) if err != nil { log.Fatal().Err(err).Msgf("failure loading certs") } // build server config s.l3afdServer.TLSConfig = &tls.Config{ Certificates: []tls.Certificate{serverCert}, GetConfigForClient: func(hi *tls.ClientHelloInfo) (*tls.Config, error) { serverConf := &tls.Config{ Certificates: []tls.Certificate{serverCert}, MinVersion: tls.VersionTLS12, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: s.CaCertPool, VerifyPeerCertificate: s.getClientValidator(hi), } return serverConf, nil }, } cpb, _ := pem.Decode(caCert) cert, err := x509.ParseCertificate(cpb.Bytes) if err != nil { log.Fatal().Err(err).Msgf("error in parsing tls certificate : %v", conf.MTLSCACertFilename) } expiry := cert.NotAfter start := cert.NotBefore go func() { period := time.Hour * 24 ticker := time.NewTicker(period) defer ticker.Stop() for { select { case <-ticker.C: MonitorTLS(start, expiry, conf) case <-ctx.Done(): return } } }() if err := s.l3afdServer.ServeTLS(l, serverCertFile, serverKeyFile); !errors.Is(err, http.ErrServerClosed) { log.Fatal().Err(err).Msgf("failed to start L3AFD server with mTLS enabled") } } else { log.Info().Msgf("l3afd server listening - %s ", conf.L3afConfigsRestAPIAddr) if err := s.l3afdServer.Serve(l); !errors.Is(err, http.ErrServerClosed) { log.Fatal().Err(err).Msgf("failed to start L3AFD server") } } }() return nil } func (s *Server) GracefulStop(shutdownTimeout time.Duration) error { log.Info().Msg("L3afd graceful stop initiated") exitCode := 0 if len(s.BPFRTConfigs.IngressXDPBpfs) > 0 || len(s.BPFRTConfigs.IngressTCBpfs) > 0 || len(s.BPFRTConfigs.EgressTCBpfs) > 0 || s.BPFRTConfigs.ProbesBpfs.Len() > 0 { ctx, cancelfunc := context.WithTimeout(context.Background(), shutdownTimeout) defer cancelfunc() if err := s.BPFRTConfigs.Close(ctx); err != nil { log.Error().Err(err).Msg("stopping all network functions failed") exitCode = 1 } } os.Exit(exitCode) return nil } // isLoopback - Check for localhost or loopback address func isLoopback(addr string) bool { if strings.Contains(addr, "localhost:") { return true } if id := strings.LastIndex(addr, ":"); id > -1 { addr = addr[:id] } if ipAddr := net.ParseIP(addr); ipAddr != nil { return ipAddr.IsLoopback() } // :port scenario return true } func MonitorTLS(start time.Time, expiry time.Time, conf *config.Config) { todayDate := time.Now() expiryDate := expiry startDate := start diff := expiryDate.Sub(todayDate) remainingHoursToStart := todayDate.Sub(startDate) limit := conf.MTLSCertExpiryWarningDays * 24 remainingHoursToExpire := int(diff.Hours()) if remainingHoursToStart > 0 { log.Fatal().Msgf("tls certificate start from : %v", startDate) } if remainingHoursToExpire <= limit { if remainingHoursToExpire < 0 { log.Fatal().Msgf("tls certificate is expired on : %v", expiryDate) } else { log.Warn().Msgf("tls certificate will expire in %v days", int64(remainingHoursToExpire/24)) } } } func (s *Server) getClientValidator(helloInfo *tls.ClientHelloInfo) func([][]byte, [][]*x509.Certificate) error { log.Debug().Msgf("Inside get client validator - %v", helloInfo.Conn.RemoteAddr()) return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { // Verifying client certs with root ca opts := x509.VerifyOptions{ Roots: s.CaCertPool, CurrentTime: time.Now(), Intermediates: x509.NewCertPool(), KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } _, err := verifiedChains[0][0].Verify(opts) if err != nil { log.Error().Err(err).Msgf("certs verification failed") return err } log.Debug().Msgf("validating with SAN match rules - %s", s.SANMatchRules) if len(s.SANMatchRules) == 0 { return nil } for _, dnsName := range verifiedChains[0][0].DNSNames { if !validHostname(dnsName, true) { continue } dnsName = toLowerCaseASCII(dnsName) for _, sanMatchRule := range s.SANMatchRules { sanMatchRule = toLowerCaseASCII(sanMatchRule) if matchExactly(dnsName, sanMatchRule) { log.Debug().Msgf("Successfully matched matchExactly cert dns %s SANMatchRule %s", dnsName, sanMatchRule) return nil } else if matchHostnamesWithRegexp(dnsName, sanMatchRule) { log.Debug().Msgf("Successfully matched matchHostnamesWithRegexp cert dns %s SANMatchRule %s", dnsName, sanMatchRule) return nil } } } err = errors.New("certs verification with SAN match not found") log.Error().Err(err).Msgf("SAN match rules %s", s.SANMatchRules) return err } } // toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use // an explicitly ASCII function to avoid any sharp corners resulting from // performing Unicode operations on DNS labels. func toLowerCaseASCII(in string) string { // If the string is already lower-case then there's nothing to do. isAlreadyLowerCase := true for _, c := range in { if c == utf8.RuneError { // If we get a UTF-8 error then there might be // upper-case ASCII bytes in the invalid sequence. isAlreadyLowerCase = false break } if 'A' <= c && c <= 'Z' { isAlreadyLowerCase = false break } } if isAlreadyLowerCase { return in } out := []byte(in) for i, c := range out { if 'A' <= c && c <= 'Z' { out[i] += 'a' - 'A' } } return string(out) } // validHostname reports whether host is a valid hostname that can be matched or // matched against according to RFC 6125 2.2, with some leniency to accommodate // legacy values. func validHostname(host string, isPattern bool) bool { if !isPattern { host = strings.TrimSuffix(host, ".") } if len(host) == 0 { return false } for i, part := range strings.Split(host, ".") { if part == "" { // Empty label. return false } if isPattern && i == 0 && part == "*" { // Only allow full left-most wildcards, as those are the only ones // we match, and matching literal '*' characters is probably never // the expected behavior. continue } for j, c := range part { if 'a' <= c && c <= 'z' { continue } if '0' <= c && c <= '9' { continue } if 'A' <= c && c <= 'Z' { continue } if c == '-' && j != 0 { continue } if c == '_' { // Not a valid character in hostnames, but commonly // found in deployments outside the WebPKI. continue } return false } } return true } // matchExactly - match hostnames func matchExactly(hostA, hostB string) bool { // Here checking hostB (i.e. sanMatchRule) is valid hostname and not regex/pattern if !validHostname(hostB, false) { return false } return hostA == hostB } // matchHostnamesWithRegexp - To match the san rules with regexp func matchHostnamesWithRegexp(dnsName, sanMatchRule string) bool { defer func() bool { if err := recover(); err != nil { log.Warn().Msgf("panic occurred: %v", err) } return false }() if len(dnsName) == 0 || len(sanMatchRule) == 0 { return false } re := regexp.MustCompile(sanMatchRule) return re.MatchString(dnsName) } ================================================ FILE: apis/configwatch_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package apis import ( "reflect" "testing" ) func TestMatchHostnamesWithRegexp(t *testing.T) { type args struct { dnsName string sanMatchRule string } tests := []struct { name string args args want bool wantErr bool }{ { name: "EmptyCheck", args: args{dnsName: "", sanMatchRule: ""}, want: false, wantErr: false, }, { name: "LengthMissMatchCheck", args: args{dnsName: "l3afd-lfn.us.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, want: false, wantErr: false, }, { name: "LengthMatchCheck", args: args{dnsName: "l3afd-lfn.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, want: true, wantErr: false, }, { name: "LengthMatchPatternMissCheck", args: args{dnsName: "l3afd-us.l3af.io", sanMatchRule: "l3afd-lf.l3af.io"}, want: false, wantErr: false, }, { name: "PatternMatchCheck", args: args{dnsName: "l3afd-*.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, want: false, wantErr: false, }, { name: "PatternMissMatchCheck", args: args{dnsName: "*l3afd-.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, want: false, wantErr: true, }, { name: "PatternRegExMatchCheck", args: args{dnsName: "asnl3afd-lfn.l3af.io", sanMatchRule: ".*l3afd-lfn.l3af.io"}, want: true, wantErr: false, }, { name: "PatternRegExExactMatchCheck", args: args{dnsName: "l3afd-dev.l3af.io", sanMatchRule: "^dev.l3af.io$"}, want: false, wantErr: false, }, { name: "PatternRegExFindMatch", args: args{dnsName: "l3afd-dev.l3af.io", sanMatchRule: "dev.l3af.io"}, want: true, wantErr: false, }, { name: "PatternRegExFindMatchPattern", args: args{dnsName: "l3afd-dev-10.l3af.io", sanMatchRule: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, want: true, wantErr: false, }, { name: "PatternRegExLowerCaseMatch", args: args{dnsName: "l3afd-dev-a0.l3af.io", sanMatchRule: "^l3afd-dev-[a-z][0-9]\\.l3af\\.io$"}, want: true, wantErr: false, }, { name: "PatternRegExUpperCaseMatch", args: args{dnsName: "l3afd-dev-A0.l3af.io", sanMatchRule: "^l3afd-dev-[A-Z][0-9]\\.l3af\\.io$"}, want: true, wantErr: false, }, { name: "PatternRegExPanicCheck", args: args{dnsName: "l3afd-dev-A0.l3af.io", sanMatchRule: "*l3afd-dev.l3af.io"}, want: false, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := matchHostnamesWithRegexp(tt.args.dnsName, tt.args.sanMatchRule) if !reflect.DeepEqual(got, tt.want) { t.Errorf("matchHostnamesWithRegexp() = %v, want %v", got, tt.want) } }) } } func TestMatchExactly(t *testing.T) { type args struct { hostA string hostB string } tests := []struct { name string args args want bool wantErr bool }{ { name: "EmptyCheck", args: args{hostA: "", hostB: ""}, want: false, wantErr: false, }, { name: "ExactMatchCheck", args: args{hostA: "l3afd-lfn.l3af.io", hostB: "l3afd-lfn.l3af.io"}, want: true, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := matchExactly(tt.args.hostA, tt.args.hostB) if !reflect.DeepEqual(got, tt.want) { t.Errorf("matchHostnames() = %v, want %v", got, tt.want) } }) } } func TestToLowerCaseASCII(t *testing.T) { type args struct { in string } tests := []struct { name string args args want string wantErr bool }{ { name: "EmptyCheck", args: args{in: ""}, want: "", wantErr: false, }, { name: "RegexLowerCheck", args: args{in: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, want: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$", wantErr: false, }, { name: "RegexUpperValueCheck", args: args{in: "^L3AFd-dev-[0-9][0-9]\\.l3af\\.io$"}, want: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$", wantErr: false, }, { name: "RegexLowerCheckRuneError", args: args{in: "^�l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, want: "^�l3afd-dev-[0-9][0-9]\\.l3af\\.io$", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := toLowerCaseASCII(tt.args.in) if !reflect.DeepEqual(got, tt.want) { t.Errorf("matchHostnames() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: apis/handlers/addprog.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/rs/zerolog/log" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/models" ) // AddEbpfPrograms add new eBPF programs on node // @Summary Adds new eBPF Programs on node // @Description Adds new eBPF Programs on node // @Accept json // @Produce json // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" // @Success 200 // @Router /l3af/configs/v1/add [post] func AddEbpfPrograms(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) if models.IsReadOnly { log.Warn().Msgf("We are in between restart please try after some time") mesg = "We are currently in the middle of a restart. Please attempt again after a while." return } defer DecWriteReq() IncWriteReq() if r.Body == nil { log.Warn().Msgf("Empty request body") return } bodyBuffer, err := io.ReadAll(r.Body) if err != nil { mesg = fmt.Sprintf("failed to read request body: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } var t []models.L3afBPFPrograms if err := json.Unmarshal(bodyBuffer, &t); err != nil { mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } if err := bpfcfg.AddeBPFPrograms(t); err != nil { mesg = fmt.Sprintf("failed to AddEbpfPrograms : %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } } } func IncWriteReq() { models.StateLock.Lock() models.CurrentWriteReq++ models.StateLock.Unlock() } func DecWriteReq() { models.StateLock.Lock() models.CurrentWriteReq-- models.StateLock.Unlock() } ================================================ FILE: apis/handlers/addprog_test.go ================================================ package handlers import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" ) const dummypayload string = `[ { "host_name" : "l3af-local-test", "iface" : "fakeif0", "bpf_programs" : { "xdp_ingress" : [ ], "tc_egress": [ ], "tc_ingress": [ ] } } ] ` func Test_addprog(t *testing.T) { tests := []struct { name string Body *strings.Reader header map[string]string status int cfg *bpfprogs.NFConfigs isreadonly bool }{ { name: "NilBody", Body: nil, status: http.StatusOK, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "FailedToUnmarshal", Body: strings.NewReader("Something"), status: http.StatusInternalServerError, header: map[string]string{}, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "EmptyInput", Body: strings.NewReader("[]"), header: map[string]string{ "Content-Type": "application/json", }, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, status: http.StatusOK, }, { name: "UnknownHostName", Body: strings.NewReader(dummypayload), status: http.StatusInternalServerError, header: map[string]string{}, cfg: &bpfprogs.NFConfigs{ HostName: "dummy", HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, isreadonly: false, }, { name: "InReadonly", Body: nil, header: map[string]string{ "Content-Type": "application/json", }, isreadonly: true, cfg: nil, status: http.StatusOK, }, } for _, tt := range tests { var req *http.Request if tt.Body == nil { req, _ = http.NewRequest("POST", "/l3af/configs/v1/add", nil) } else { req, _ = http.NewRequest("POST", "/l3af/configs/v1/add", tt.Body) } for key, val := range tt.header { req.Header.Set(key, val) } models.IsReadOnly = tt.isreadonly rr := httptest.NewRecorder() handler := AddEbpfPrograms(context.Background(), tt.cfg) handler.ServeHTTP(rr, req) if rr.Code != tt.status { models.IsReadOnly = false t.Error("AddEbpfPrograms Failed") } models.IsReadOnly = false } } ================================================ FILE: apis/handlers/deleteprog.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/rs/zerolog/log" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/models" ) // DeleteEbpfPrograms remove eBPF programs on node // @Summary Removes eBPF Programs on node // @Description Removes eBPF Programs on node // @Accept json // @Produce json // @Param cfgs body []models.L3afBPFProgramNames true "BPF program names" // @Success 200 // @Router /l3af/configs/v1/delete [post] func DeleteEbpfPrograms(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) if models.IsReadOnly { log.Warn().Msgf("We are in between restart please try after some time") mesg = "We are currently in the middle of a restart. Please attempt again after a while." return } defer DecWriteReq() IncWriteReq() if r.Body == nil { log.Warn().Msgf("Empty request body") return } bodyBuffer, err := io.ReadAll(r.Body) if err != nil { mesg = fmt.Sprintf("failed to read request body: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } var t []models.L3afBPFProgramNames if err := json.Unmarshal(bodyBuffer, &t); err != nil { mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } if err := bpfcfg.DeleteEbpfPrograms(t); err != nil { mesg = fmt.Sprintf("failed to DeleteEbpfPrograms : %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } } } ================================================ FILE: apis/handlers/deleteprog_test.go ================================================ package handlers import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" ) const payloadfordelete string = `[ { "host_name": "l3af-local-test", "iface": "fakeif0", "bpf_programs": { "xdp_ingress": [ "ratelimiting", "connection-limit" ] } } ] ` func Test_DeleteEbpfPrograms(t *testing.T) { tests := []struct { name string Body *strings.Reader header map[string]string status int cfg *bpfprogs.NFConfigs isreadonly bool }{ { name: "NilBody", Body: nil, status: http.StatusOK, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "FailedToUnmarshal", Body: strings.NewReader("Something"), status: http.StatusInternalServerError, header: map[string]string{}, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "EmptyInput", Body: strings.NewReader(`[]`), header: map[string]string{ "Content-Type": "application/json", }, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, status: http.StatusOK, }, { name: "UnknownHostName", Body: strings.NewReader(payloadfordelete), status: http.StatusInternalServerError, header: map[string]string{}, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostName: "dummy", HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "InReadonly", Body: nil, status: http.StatusOK, header: map[string]string{ "Content-Type": "application/json", }, isreadonly: true, cfg: nil, }, } for _, tt := range tests { var req *http.Request if tt.Body == nil { req, _ = http.NewRequest("POST", "/l3af/configs/v1/delete", nil) } else { req, _ = http.NewRequest("POST", "/l3af/configs/v1/delete", tt.Body) } for key, val := range tt.header { req.Header.Set(key, val) } models.IsReadOnly = tt.isreadonly rr := httptest.NewRecorder() handler := DeleteEbpfPrograms(context.Background(), tt.cfg) handler.ServeHTTP(rr, req) if rr.Code != tt.status { models.IsReadOnly = false t.Error("DeleteEbpfPrograms Failed") } models.IsReadOnly = false } } ================================================ FILE: apis/handlers/getconfig.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "encoding/json" "net/http" chi "github.com/go-chi/chi/v5" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/rs/zerolog/log" ) var bpfcfgs *bpfprogs.NFConfigs func InitConfigs(cfgs *bpfprogs.NFConfigs) error { bpfcfgs = cfgs return nil } // GetConfig Returns details of the configuration of eBPF Programs for a given interface // @Summary Returns details of the configuration of eBPF Programs for a given interface // @Description Returns details of the configuration of eBPF Programs for a given interface // @Accept json // @Produce json // @Param iface path string true "interface name" // @Success 200 // @Router /l3af/configs/v1/{iface} [get] func GetConfig(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) iface := chi.URLParam(r, "iface") if len(iface) == 0 { mesg = "iface value is empty" log.Error().Msg(mesg) statusCode = http.StatusBadRequest return } resp, err := json.MarshalIndent(bpfcfgs.EBPFPrograms(iface), "", " ") if err != nil { mesg = "internal server error" log.Error().Msgf("failed to marshal response: %v", err) statusCode = http.StatusInternalServerError return } mesg = string(resp) } // GetConfigAll Returns details of the configuration of eBPF Programs for all interfaces on a node // @Summary Returns details of the configuration of eBPF Programs for all interfaces on a node // @Description Returns details of the configuration of eBPF Programs for all interfaces on a node // @Accept json // @Produce json // @Success 200 // @Router /l3af/configs/v1 [get] func GetConfigAll(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) resp, err := json.MarshalIndent(bpfcfgs.EBPFProgramsAll(), "", " ") if err != nil { mesg = "internal server error" log.Error().Msgf("failed to marshal response: %v", err) statusCode = http.StatusInternalServerError return } mesg = string(resp) } ================================================ FILE: apis/handlers/getconfig_test.go ================================================ package handlers import ( "container/list" "context" "net/http" "net/http/httptest" "testing" chi "github.com/go-chi/chi/v5" "github.com/l3af-project/l3afd/v2/bpfprogs" ) func Test_GetConfig(t *testing.T) { tests := []struct { name string iface string status int cfg *bpfprogs.NFConfigs }{ { name: "EmptyInterfaceInRequest", iface: "", status: http.StatusBadRequest, cfg: &bpfprogs.NFConfigs{}, }, { name: "GoodInput", iface: "fakeif0", status: http.StatusOK, cfg: &bpfprogs.NFConfigs{ IngressXDPBpfs: map[string]*list.List{"fakeif0": nil}, IngressTCBpfs: map[string]*list.List{"fakeif0": nil}, EgressTCBpfs: map[string]*list.List{"fakeif0": nil}, }, }, } for _, tt := range tests { req, _ := http.NewRequest("GET", "l3af/configs/v1/"+tt.iface, nil) rctx := chi.NewRouteContext() req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rctx.URLParams.Add("iface", tt.iface) rr := httptest.NewRecorder() handler := http.HandlerFunc(GetConfig) InitConfigs(tt.cfg) handler.ServeHTTP(rr, req) if rr.Code != tt.status { t.Errorf("GetConfig Failed") } } } func Test_GetConfigAll(t *testing.T) { tests := []struct { name string status int cfg *bpfprogs.NFConfigs }{ { name: "GoodInput", status: http.StatusOK, cfg: &bpfprogs.NFConfigs{}, }, } for _, tt := range tests { req, _ := http.NewRequest("GET", "l3af/configs/v1", nil) rr := httptest.NewRecorder() handler := http.HandlerFunc(GetConfigAll) InitConfigs(tt.cfg) handler.ServeHTTP(rr, req) if rr.Code != tt.status { t.Errorf("GetConfigAll Failed") } } } ================================================ FILE: apis/handlers/restart_linux.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "encoding/gob" "encoding/json" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" "net/http" "github.com/rs/zerolog/log" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/pidfile" "github.com/l3af-project/l3afd/v2/restart" ) // HandleRestart will start new instance of l3afd provided by payload // @Summary this api will start new instance of l3afd provided by payload // @Description this api will start new instance of l3afd provided by payload // @Accept json // @Produce json // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" // @Success 200 // @Router /l3af/configs/v1/restart [put] func HandleRestart(bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) if models.IsReadOnly { log.Warn().Msgf("We are in between restart please try after some time") mesg = "We are currently in the middle of a restart. Please attempt again after a while." statusCode = http.StatusInternalServerError return } if r.Body == nil { mesg = "nil request body" statusCode = http.StatusInternalServerError log.Warn().Msgf("Empty request body") return } bodyBuffer, err := io.ReadAll(r.Body) if err != nil { mesg = fmt.Sprintf("failed to read request body: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } var t models.RestartConfig if err := json.Unmarshal(bodyBuffer, &t); err != nil { mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } machineHostname, err := os.Hostname() if err != nil { mesg = "failed to get os hostname" log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } if machineHostname != t.HostName { mesg = "this api request is not for provided host" log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } defer func() { models.IsReadOnly = false }() models.IsReadOnly = true // complete active requests for { models.StateLock.Lock() if models.CurrentWriteReq == 0 { models.StateLock.Unlock() break } models.StateLock.Unlock() time.Sleep(5 * time.Millisecond) } // Now our system is in Readonly state oldCfgPath, err := restart.ReadSymlink(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg")) if err != nil { mesg = fmt.Sprintf("failed read symlink %v with error: %v", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg"), err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } oldBinPath, err := restart.ReadSymlink(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd")) if err != nil { mesg = fmt.Sprintf("failed read symlink %v with error: %v", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd"), err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } // /usr/local/l3afd/v2.0.0/l3afd/l3afd --> v2.0.0/l3afd/l3afd --> v2.0.0 oldVersion := strings.Split(strings.Trim(oldBinPath, bpfcfg.HostConfig.BasePath+"/"), "/")[0] if _, ok := models.AvailableVersions[t.Version]; !ok { mesg = "invalid version to upgrade" log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } err = restart.GetNewVersion(models.L3AFDRestartArtifactName, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) if err != nil { mesg = fmt.Sprintf("failed to get new version: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) return } bpfProgs := bpfcfg.GetL3AFHOSTDATA() ln, err := net.Listen("unix", models.HostSock) if err != nil { log.Err(err) err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) statusCode = http.StatusInternalServerError return } srvError := make(chan error, 1) go func() { defer ln.Close() conn, err := ln.Accept() if err != nil { log.Err(err) srvError <- err return } defer conn.Close() encoder := gob.NewEncoder(conn) err = encoder.Encode(bpfProgs) if err != nil { log.Err(err) srvError <- err return } srvError <- nil }() files := make([]*os.File, 3) srvToIndex := make(map[string]int) srvToIndex["stat_http"] = 0 srvToIndex["main_http"] = 1 srvToIndex["debug_http"] = 2 isErr := false models.AllNetListeners.Range(func(srvr, listr interface{}) bool { // iterate over the map srv, _ := srvr.(string) lis, _ := listr.(*net.TCPListener) idx := srvToIndex[srv] lf, err := lis.File() if err != nil { log.Error().Msgf("%v", err) err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) statusCode = http.StatusInternalServerError isErr = true return false } newFile := os.NewFile(uintptr(lf.Fd()), "dupFdlistner"+strconv.Itoa(idx)) files[idx] = newFile return true }) if isErr { return } cmd := exec.Command(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd"), "--config", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg")) cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.ExtraFiles = files err = bpfcfg.StopAllProbesAndUserPrograms() if err != nil { log.Err(err) err = bpfcfg.StartAllUserProgramsAndProbes() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) } err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) statusCode = http.StatusInternalServerError return } log.Info().Msg("Starting child Process") err = cmd.Start() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to start new instance %v", err) err = cmd.Process.Kill() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) } err = bpfcfg.StartAllUserProgramsAndProbes() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) } err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) } err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) if err != nil { mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) } statusCode = http.StatusInternalServerError return } NewProcessStatus := make(chan string) go func() { // I need to write client code for reading the state of new process var err error var conn net.Conn f := false for i := 1; i <= bpfcfg.HostConfig.TimetoRestart; i++ { conn, err = net.Dial("unix", models.StateSock) if err == nil { f = true break } log.Info().Msgf("Waiting for socket to be up...") time.Sleep(time.Second) // sleep for a second before trying again } if !f { conn.Close() NewProcessStatus <- models.StatusFailed return } defer conn.Close() decoder := gob.NewDecoder(conn) var data string err = decoder.Decode(&data) if err != nil { NewProcessStatus <- models.StatusFailed return } NewProcessStatus <- data }() // time to bootup select { case terr := <-srvError: if terr != nil { statusCode = http.StatusInternalServerError err = cmd.Process.Kill() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) } err = bpfcfg.StartAllUserProgramsAndProbes() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) } err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) } err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) if err != nil { mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) } statusCode = http.StatusInternalServerError log.Err(terr) return } break default: time.Sleep(time.Second) } st := <-NewProcessStatus if st == models.StatusFailed { err = cmd.Process.Kill() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) } err = bpfcfg.StartAllUserProgramsAndProbes() if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) } err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) if err != nil { log.Error().Msgf("%v", err) mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) } err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) if err != nil { mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) } statusCode = http.StatusInternalServerError return } else { log.Info().Msgf("doing exiting old process") models.CloseForRestart <- struct{}{} } } } ================================================ FILE: apis/handlers/restart_linux_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "net/http" "net/http/httptest" "testing" "github.com/l3af-project/l3afd/v2/models" ) func Test_HandleRestart(t *testing.T) { var req *http.Request req, _ = http.NewRequest("PUT", "/l3af/configs/v1/restart", nil) req.Header.Set("Content-Type", "application/json") models.IsReadOnly = true rr := httptest.NewRecorder() handler := HandleRestart(nil) handler.ServeHTTP(rr, req) if rr.Code != http.StatusInternalServerError { models.IsReadOnly = false t.Error("Handle restart Failed") } models.IsReadOnly = false } ================================================ FILE: apis/handlers/restart_windows.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "net/http" "github.com/rs/zerolog/log" "github.com/l3af-project/l3afd/v2/bpfprogs" ) // HandleRestart Store meta data about ebpf programs and exit // @Summary Store meta data about ebpf programs and exit // @Description Store meta data about ebpf programs and exit // @Accept json // @Produce json // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" // @Success 200 // @Router /l3af/configs/v1/restart [put] func HandleRestart(bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) mesg = "Graceful restart is only supported for linux as of now" } } ================================================ FILE: apis/handlers/restart_windows_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "net/http" "net/http/httptest" "testing" ) func Test_HandleRestart(t *testing.T) { var req *http.Request req, _ = http.NewRequest("PUT", "/l3af/configs/v1/restart", nil) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() handler := HandleRestart(nil) handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Error("Handle restart Failed") } } ================================================ FILE: apis/handlers/updateconfig.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package handlers import ( "context" "encoding/json" "fmt" "io" "net/http" "github.com/rs/zerolog/log" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/models" ) // UpdateConfig Update eBPF Programs configuration // @Summary Update eBPF Programs configuration // @Description Update eBPF Programs configuration // @Accept json // @Produce json // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" // @Success 200 // @Router /l3af/configs/v1/update [post] func UpdateConfig(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mesg := "" statusCode := http.StatusOK w.Header().Add("Content-Type", "application/json") defer func(mesg *string, statusCode *int) { w.WriteHeader(*statusCode) _, err := w.Write([]byte(*mesg)) if err != nil { log.Warn().Msgf("Failed to write response bytes: %v", err) } }(&mesg, &statusCode) if models.IsReadOnly { log.Warn().Msgf("We are in between restart please try after some time") mesg = "We are currently in the middle of a restart. Please attempt again after a while." return } defer DecWriteReq() IncWriteReq() if r.Body == nil { log.Warn().Msgf("Empty request body") return } bodyBuffer, err := io.ReadAll(r.Body) if err != nil { mesg = fmt.Sprintf("failed to read request body: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } var t []models.L3afBPFPrograms if err := json.Unmarshal(bodyBuffer, &t); err != nil { mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } if err := bpfcfg.DeployeBPFPrograms(t); err != nil { mesg = fmt.Sprintf("failed to deploy ebpf programs: %v", err) log.Error().Msg(mesg) statusCode = http.StatusInternalServerError return } } } ================================================ FILE: apis/handlers/updateconfig_test.go ================================================ package handlers import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" ) func Test_UpdateConfig(t *testing.T) { tests := []struct { name string Body *strings.Reader header map[string]string status int cfg *bpfprogs.NFConfigs isreadonly bool }{ { name: "NilBody", Body: nil, status: http.StatusOK, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "FailedToUnmarshal", Body: strings.NewReader("Something"), status: http.StatusInternalServerError, header: map[string]string{}, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "UnknownHostName", Body: strings.NewReader(dummypayload), status: http.StatusInternalServerError, header: map[string]string{}, isreadonly: false, cfg: &bpfprogs.NFConfigs{ HostName: "dummy", HostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), }, }, }, { name: "InReadonly", Body: nil, status: http.StatusOK, header: map[string]string{ "Content-Type": "application/json", }, isreadonly: true, cfg: nil, }, } for _, tt := range tests { var req *http.Request if tt.Body == nil { req, _ = http.NewRequest("POST", "/l3af/configs/v1/update", nil) } else { req, _ = http.NewRequest("POST", "/l3af/configs/v1/update", tt.Body) } for key, val := range tt.header { req.Header.Set(key, val) } models.IsReadOnly = tt.isreadonly rr := httptest.NewRecorder() handler := UpdateConfig(context.Background(), tt.cfg) handler.ServeHTTP(rr, req) if rr.Code != tt.status { models.IsReadOnly = false t.Error("UpdateConfig Failed") } models.IsReadOnly = false } } ================================================ FILE: apis/routes.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package apis import ( "context" "github.com/l3af-project/l3afd/v2/apis/handlers" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/routes" ) func apiRoutes(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) []routes.Route { r := []routes.Route{ { Method: "POST", Path: "/l3af/configs/{version}/update", HandlerFunc: handlers.UpdateConfig(ctx, bpfcfg), }, { Method: "GET", Path: "/l3af/configs/{version}/{iface}", HandlerFunc: handlers.GetConfig, }, { Method: "GET", Path: "/l3af/configs/{version}", HandlerFunc: handlers.GetConfigAll, }, { Method: "POST", Path: "/l3af/configs/{version}/add", HandlerFunc: handlers.AddEbpfPrograms(ctx, bpfcfg), }, { Method: "POST", Path: "/l3af/configs/{version}/delete", HandlerFunc: handlers.DeleteEbpfPrograms(ctx, bpfcfg), }, { Method: "PUT", Path: "/l3af/configs/{version}/restart", HandlerFunc: handlers.HandleRestart(bpfcfg), }, } return r } ================================================ FILE: bpfprogs/bpf.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package bpfprogs provides primitives for BPF programs / Network Functions. package bpfprogs import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "container/ring" "context" "errors" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path" "path/filepath" "strconv" "strings" "sync" "time" "unsafe" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/l3af-project/l3afd/v2/utils" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/rlimit" ps "github.com/mitchellh/go-ps" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" ) var ( execCommand = exec.Command copyBufPool sync.Pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} ) //lint:ignore U1000 avoid false linter error on windows, since this variable is only used in linux code const executePerm uint32 = 0111 const bpfStatus string = "RUNNING" // BPF defines run time details for BPFProgram. type BPF struct { Program models.BPFProgram Cmd *exec.Cmd `json:"-"` FilePath string // Binary file path RestartCount int // To track restart count PrevMapNamePath string // Previous Map name with path to link MapNamePath string // Map name with path ProgID ebpf.ProgramID // eBPF Program ID BpfMaps map[string]BPFMap // Config maps passed as map-args, Map name is Key MetricsBpfMaps map[string]*MetricsBPFMap // Metrics map name+key+aggregator is key Ctx context.Context `json:"-"` Done chan bool `json:"-"` ProgMapCollection *ebpf.Collection `json:"_"` // eBPF Collection reference ProgMapID ebpf.MapID // Prog map id PrevProgMapID ebpf.MapID // Prev prog map id HostConfig *config.Config Link link.Link `json:"-"` // handle link object ProbeLinks []*link.Link } func NewBpfProgram(ctx context.Context, program models.BPFProgram, conf *config.Config, ifaceName string) *BPF { var progMapFilePath string version := utils.ReplaceDotsWithUnderscores(program.Version) if len(program.MapName) > 0 { if program.ProgType == models.XDPType { progMapFilePath = filepath.Join(conf.BpfMapDefaultPath, ifaceName, program.Name, version, program.MapName) } else if program.ProgType == models.TCType { progMapFilePath = filepath.Join(conf.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, program.Name, version, program.MapName) } if strings.Contains(progMapFilePath, "..") { log.Error().Msgf("program map file contains relative path %s", progMapFilePath) return nil } } bpf := &BPF{ Program: program, RestartCount: 0, Cmd: nil, FilePath: "", BpfMaps: make(map[string]BPFMap, 0), MetricsBpfMaps: make(map[string]*MetricsBPFMap, 0), Ctx: ctx, Done: nil, HostConfig: conf, MapNamePath: progMapFilePath, } return bpf } // LoadRootProgram - Loading the Root Program for a given interface. func LoadRootProgram(ifaceName string, direction string, progType string, conf *config.Config) (*BPF, error) { log.Info().Msgf("LoadRootProgram iface %s direction %s progType %s", ifaceName, direction, progType) var rootProgBPF *BPF switch progType { case models.XDPType: version := utils.ReplaceDotsWithUnderscores(conf.XDPRootVersion) rootProgBPF = &BPF{ Program: models.BPFProgram{ Name: conf.XDPRootPackageName, Artifact: conf.XDPRootArtifact, MapName: conf.XDPRootMapName, Version: conf.XDPRootVersion, UserProgramDaemon: false, AdminStatus: models.Enabled, ProgType: models.XDPType, SeqID: 0, StartArgs: map[string]interface{}{}, StopArgs: map[string]interface{}{}, StatusArgs: map[string]interface{}{}, ObjectFile: conf.XDPRootObjectFile, EntryFunctionName: conf.XDPRootEntryFunctionName, }, RestartCount: 0, Cmd: nil, FilePath: "", PrevMapNamePath: "", HostConfig: conf, MapNamePath: filepath.Join(conf.BpfMapDefaultPath, ifaceName, conf.XDPRootPackageName, version, conf.XDPRootMapName), Link: nil, } case models.TCType: rootProgBPF = &BPF{ Program: models.BPFProgram{ Name: conf.TCRootPackageName, Artifact: conf.TCRootArtifact, Version: conf.TCRootVersion, UserProgramDaemon: false, AdminStatus: models.Enabled, ProgType: models.TCType, StartArgs: map[string]interface{}{}, StopArgs: map[string]interface{}{}, StatusArgs: map[string]interface{}{}, }, RestartCount: 0, Cmd: nil, FilePath: "", PrevMapNamePath: "", HostConfig: conf, } if direction == models.IngressType { rootProgBPF.Program.MapName = conf.TCRootIngressMapName rootProgBPF.Program.ObjectFile = conf.TCRootIngressObjectFile rootProgBPF.Program.EntryFunctionName = conf.TCRootIngressEntryFunctionName } else if direction == models.EgressType { rootProgBPF.Program.MapName = conf.TCRootEgressMapName rootProgBPF.Program.ObjectFile = conf.TCRootEgressObjectFile rootProgBPF.Program.EntryFunctionName = conf.TCRootEgressEntryFunctionName } version := utils.ReplaceDotsWithUnderscores(rootProgBPF.Program.Version) rootProgBPF.MapNamePath = filepath.Join(conf.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, rootProgBPF.Program.Name, version, rootProgBPF.Program.MapName) default: return nil, fmt.Errorf("unknown direction %s for root program in iface %s", direction, ifaceName) } if err := rootProgBPF.VerifyAndGetArtifacts(conf); err != nil { log.Error().Err(err).Msg("failed to get root artifacts") return nil, err } // On l3afd crashing scenario verify root program are unloaded properly by checking existence of persisted maps // if map file exists then root program didn't clean up pinned map files if fileExists(rootProgBPF.MapNamePath) { log.Warn().Msgf("previous instance of root program %s persisted map %s file exists", rootProgBPF.Program.Name, rootProgBPF.MapNamePath) if err := rootProgBPF.RemoveRootProgMapFile(ifaceName); err != nil { log.Warn().Err(err).Msgf("previous instance of root program %s map file not removed successfully - %s ", rootProgBPF.Program.Name, rootProgBPF.MapNamePath) } } version := utils.ReplaceDotsWithUnderscores(rootProgBPF.Program.Version) if progType == models.XDPType { rlimit.RemoveMemlock() if err := rootProgBPF.LoadXDPAttachProgram(ifaceName); err != nil { return nil, fmt.Errorf("failed to load xdp root program on iface %s name %s direction %s with err %w", ifaceName, rootProgBPF.Program.Name, direction, err) } // pin the program also progPinPath := utils.ProgPinPath(rootProgBPF.HostConfig.BpfMapDefaultPath, ifaceName, rootProgBPF.Program.Name, version, rootProgBPF.Program.EntryFunctionName, rootProgBPF.Program.ProgType) if err := rootProgBPF.ProgMapCollection.Programs[rootProgBPF.Program.EntryFunctionName].Pin(progPinPath); err != nil { return nil, err } } else { if utils.CheckTCXSupport() { if err := rootProgBPF.LoadTCXAttachProgram(ifaceName, direction); err != nil { return nil, fmt.Errorf("failed to load tcx root program on iface %s name %s direction %s with err %w", ifaceName, rootProgBPF.Program.Name, direction, err) } } else { if err := rootProgBPF.LoadTCAttachProgram(ifaceName, direction); err != nil { return nil, fmt.Errorf("failed to load tc root program on iface %s name %s direction %s with err %w", ifaceName, rootProgBPF.Program.Name, direction, err) } } // pin the program also progPinPath := utils.ProgPinPath(rootProgBPF.HostConfig.BpfMapDefaultPath, ifaceName, rootProgBPF.Program.Name, version, rootProgBPF.Program.EntryFunctionName, rootProgBPF.Program.ProgType) if err := rootProgBPF.ProgMapCollection.Programs[rootProgBPF.Program.EntryFunctionName].Pin(progPinPath); err != nil { return nil, err } } return rootProgBPF, nil } // Stop the NF process if running outside l3afd func StopExternalRunningProcess(processName string) error { // validate process name if len(processName) < 1 { return fmt.Errorf("process name can not be empty") } // process names are truncated to 15 chars psName := processName if len(processName) > 15 { psName = processName[:15] } myPid := os.Getpid() processList, err := ps.Processes() if err != nil { return fmt.Errorf("failed to fetch processes list with err %w", err) } log.Info().Msgf("Searching for process %s and not ppid %d", processName, myPid) for _, process := range processList { if strings.Contains(process.Executable(), psName) { if process.PPid() != myPid { log.Warn().Msgf("found process id %d name %s ppid %d, stopping it", process.Pid(), process.Executable(), process.PPid()) osProcess, err := os.FindProcess(process.Pid()) if err == nil { err = osProcess.Kill() } if err != nil { return fmt.Errorf("external BPFProgram stop failed with error: %w", err) } } } } return nil } // Stop returns the last error seen, but stops bpf program. // Stops the user programs if any, and unloads the BPF program. // Clean up all map handles. // Verify next program pinned map file is removed func (b *BPF) Stop(ifaceName, ipv4_address, direction string, chain bool) error { if b.Program.UserProgramDaemon && b.Cmd == nil { return fmt.Errorf("BPFProgram is not running %s", b.Program.Name) } log.Info().Msgf("Stopping BPF Program - %s", b.Program.Name) // Removing maps for key, val := range b.BpfMaps { log.Debug().Msgf("removing BPF maps %s value map %#v", key, val) delete(b.BpfMaps, key) } // Removing Metrics maps for key, val := range b.MetricsBpfMaps { log.Debug().Msgf("removing metric bpf maps %s value %#v", key, val) delete(b.MetricsBpfMaps, key) } // Stop BPF configs if len(b.Program.CmdConfig) > 0 && len(b.Program.ConfigFilePath) > 0 { log.Info().Msgf("Stopping BPF configs %s ", b.Program.Name) b.Done <- true } // Reset ProgID b.ProgID = 0 stats.Add(1, stats.BPFStopCount, b.Program.Name, direction, ifaceName, ipv4_address) // Deleting stale metrics indicates that the ebpf program is not running. stats.BPFRunning.Delete(prometheus.Labels{"ebpf_program": b.Program.Name, "version": b.Program.Version, "direction": direction, "interface_name": ifaceName, "ipv4_address": ipv4_address}) stats.BPFStartTime.Delete(prometheus.Labels{"ebpf_program": b.Program.Name, "direction": direction, "interface_name": ifaceName, "ipv4_address": ipv4_address}) // Stop User Programs if any if len(b.Program.CmdStop) < 1 && b.Program.UserProgramDaemon { // Loaded using user program if err := b.ProcessTerminate(); err != nil { return fmt.Errorf("BPFProgram %s process terminate failed with error: %w", b.Program.Name, err) } if b.Cmd != nil { if err := b.Cmd.Wait(); err != nil { log.Error().Err(err).Msgf("cmd wait at stopping bpf program %s errored", b.Program.Name) } b.Cmd = nil } } else if len(b.Program.CmdStop) > 0 && b.Program.UserProgramDaemon { cmd := filepath.Join(b.FilePath, b.Program.CmdStop) if err := assertExecutable(cmd); err != nil { return fmt.Errorf("no executable permissions on %s - error %w", b.Program.CmdStop, err) } args := make([]string, 0, len(b.Program.StopArgs)<<1) if len(ifaceName) > 0 { args = append(args, "--iface="+ifaceName) // detaching from iface } if len(direction) > 0 { args = append(args, "--direction="+direction) // xdpingress or ingress or egress } for k, val := range b.Program.StopArgs { if v, ok := val.(string); !ok { err := fmt.Errorf("stop args is not a string for the bpf program %s", b.Program.Name) log.Error().Err(err).Msgf("failed to convert stop args value into string for program %s", b.Program.Name) return err } else { args = append(args, "--"+k+" ="+v) } } log.Info().Msgf("bpf program stop command : %s %v", cmd, args) prog := execCommand(cmd, args...) if err := prog.Run(); err != nil { log.Warn().Err(err).Msgf("l3afd : Failed to stop the program %s", b.Program.CmdStop) } b.Cmd = nil } // unload the BPF programs if allInterfaces, err := getHostInterfaces(); err != nil { errOut := fmt.Errorf("failed get interfaces in Stop Function: %v", err) log.Error().Err(errOut) return errOut } else { if _, ok := allInterfaces[ifaceName]; ok { if b.ProgMapCollection != nil { if err := b.UnloadProgram(ifaceName, direction); err != nil { return fmt.Errorf("BPFProgram %s unload failed on interface %s with error: %w", b.Program.Name, ifaceName, err) } log.Info().Msgf("%s => %s direction => %s - program is unloaded/detached successfully", ifaceName, b.Program.Name, direction) } } else { if err := b.RemovePinnedFiles(ifaceName); err != nil { log.Error().Err(err).Msgf("stop user program - failed to remove map files %s", b.Program.Name) return fmt.Errorf("stop user program - failed to remove map files %s", b.Program.Name) } } } if err := b.VerifyCleanupMaps(chain); err != nil { log.Error().Err(err).Msgf("stop user program - failed to remove map files %s", b.Program.Name) return fmt.Errorf("stop user program - failed to remove map files %s", b.Program.Name) } return nil } // Start returns the last error seen, but starts bpf program. // Here initially prevprogmap entry is removed and passed to the bpf program // After starting the user program, will update the kernel progam fd into prevprogram map. // This method waits till prog fd entry is updated, else returns error assuming kernel program is not loaded. // It also verifies the next program pinned map is created or not. func (b *BPF) Start(ifaceName, ipv4_address, direction string, chain bool) error { if b.FilePath == "" { return errors.New("no program binary path found") } if len(b.Program.CmdStart) > 0 { // Verify other instance is running if err := StopExternalRunningProcess(b.Program.CmdStart); err != nil { return fmt.Errorf("failed to stop external instance of the program %s with error : %w", b.Program.CmdStart, err) } } // Making sure old map entry is removed before passing the prog fd map to the program. if len(b.PrevMapNamePath) > 0 { if err := b.RemovePrevProgFD(); err != nil { log.Error().Err(err).Msgf("ProgramMap %s entry removal failed", b.PrevMapNamePath) } } // Both xdp and tc are loaded using the same mechanism. if len(b.Program.ObjectFile) > 0 { if chain { if err := b.LoadBPFProgramChain(ifaceName, direction); err != nil { return fmt.Errorf("loading bpf program %s - error %w", b.Program.Name, err) } } else { if err := b.AttachBPFProgram(ifaceName, direction); err != nil { return fmt.Errorf("attaching bpf program %s - error %w", b.Program.Name, err) } } } else { log.Info().Msgf("bpf program object file is not defined - %s", b.Program.Name) } log.Info().Msgf("successfully Loaded & Attached %s in direction %s on interface %s", b.Program.Name, direction, ifaceName) // Start user program before loading if len(b.Program.CmdStart) > 0 { if err := b.StartUserProgram(ifaceName, direction, chain); err != nil { return fmt.Errorf("user program startup failed %s - error %w", b.Program.CmdStart, err) } } // making sure program fd map pinned file is created if err := b.VerifyPinnedProgMap(chain, true); err != nil { return fmt.Errorf("failed to find pinned file %s %w", b.MapNamePath, err) } // BPF map config values if len(b.Program.MapArgs) > 0 { if err := b.UpdateBPFMaps(ifaceName, ipv4_address, direction); err != nil { log.Error().Err(err).Msg("failed to update ebpf program BPF maps") return fmt.Errorf("failed to update ebpf program BPF maps %w", err) } } // Update args config values if len(b.Program.UpdateArgs) > 0 { if err := b.UpdateArgs(ifaceName, ipv4_address, direction); err != nil { log.Error().Err(err).Msg("failed to update ebpf program config update") return fmt.Errorf("failed to update ebpf program config update %w", err) } } // Fetch when prev program map is updated only when loaded using user program if len(b.PrevMapNamePath) > 0 && b.ProgMapCollection == nil { var err error // retry 10 times to verify entry is created for i := 0; i < 10; i++ { b.ProgID, err = b.GetProgID() if err == nil { break } log.Warn().Msg("failed to fetch the program ID, retrying after a second ... ") time.Sleep(1 * time.Second) } if err != nil { log.Error().Err(err).Msg("failed to fetch ebpf program FD") return fmt.Errorf("failed to fetch ebpf program FD %w", err) } } // BPFconfigs if len(b.Program.CmdConfig) > 0 && len(b.Program.ConfigFilePath) > 0 { log.Info().Msgf("eBPF program specific config monitoring - %s", b.Program.ConfigFilePath) b.Done = make(chan bool) go b.RunBPFConfigs() } stats.Set(float64(time.Now().Unix()), stats.BPFStartTime, b.Program.Name, direction, ifaceName, ipv4_address) userProgram, bpfProgram, err := b.isRunning() if !userProgram && !bpfProgram { log.Error().Err(err).Msg("eBPF program failed to start") return fmt.Errorf("bpf program %s failed to start %w", b.Program.Name, err) } stats.Add(1, stats.BPFStartCount, b.Program.Name, direction, ifaceName, ipv4_address) log.Info().Msgf("BPF program - %s started Program ID %d", b.Program.Name, uint32(b.ProgID)) return nil } // UpdateBPFMaps - Update the config ebpf maps via map arguments func (b *BPF) UpdateBPFMaps(ifaceName, ipv4_address, direction string) error { for _, val := range b.Program.MapArgs { bpfMap, ok := b.BpfMaps[val.Name] if !ok { if err := b.AddBPFMap(val.Name); err != nil { return err } bpfMap = b.BpfMaps[val.Name] } for _, v := range val.Args { log.Info().Msgf("Update map args key %v val %v", v.Key, v.Value) bpfMap.Update(v.Key, v.Value) } if err := bpfMap.RemoveMissingKeys(val.Args); err != nil { return fmt.Errorf("failed to remove missing entries of map %s with err %w", val.Name, err) } } stats.Add(1, stats.BPFUpdateCount, b.Program.Name, direction, ifaceName, ipv4_address) return nil } // Update config arguments using user program func (b *BPF) UpdateArgs(ifaceName, ipv4_address, direction string) error { if b.FilePath == "" { return errors.New("update - no program binary path found") } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) cmd := filepath.Join(b.FilePath, b.Program.CmdUpdate) // Validate if err := assertExecutable(cmd); err != nil { return fmt.Errorf("no executable permissions on %s - error %w", b.Program.CmdUpdate, err) } args := make([]string, 0, len(b.Program.UpdateArgs)<<1) args = append(args, "--iface="+ifaceName) // attaching to interface args = append(args, "--direction="+direction) // direction xdpingress or ingress or egress args = append(args, "--cmd="+models.UpdateType) // argument cmd to update configs args = append(args, "--version="+version) // argument version of ebpf program if len(b.HostConfig.BPFLogDir) > 1 { args = append(args, "--log-dir="+b.HostConfig.BPFLogDir) } for k, val := range b.Program.UpdateArgs { if v, ok := val.(string); !ok { err := fmt.Errorf("update args is not a string for the ebpf program %s", b.Program.Name) log.Error().Err(err).Msgf("failed to convert update args value into string for program %s", b.Program.Name) return err } else { args = append(args, "--"+k+"="+v) } } log.Info().Msgf("BPF Program update command : %s %v", cmd, args) UpdateCmd := execCommand(cmd, args...) if err := UpdateCmd.Start(); err != nil { stats.Add(1, stats.BPFUpdateFailedCount, b.Program.Name, direction, ifaceName, ipv4_address) customerr := fmt.Errorf("failed to start : %s %v %w", cmd, args, err) log.Warn().Err(customerr).Msgf("user mode BPF program failed - %s", b.Program.Name) return customerr } if err := UpdateCmd.Wait(); err != nil { stats.Add(1, stats.BPFUpdateFailedCount, b.Program.Name, direction, ifaceName, ipv4_address) return fmt.Errorf("cmd wait at starting of bpf program returned with error %w", err) } stats.Add(1, stats.BPFUpdateCount, b.Program.Name, direction, ifaceName, ipv4_address) log.Info().Msgf("BPF program - %s config updated", b.Program.Name) return nil } // Status of user program is running func (b *BPF) isRunning() (bool, bool, error) { userProgram := true var err error // CmdStatus should check for user and BPF program if len(b.Program.CmdStatus) > 1 { cmd := filepath.Join(b.FilePath, b.Program.CmdStatus) if err := assertExecutable(cmd); err != nil { userProgram = false } else { args := make([]string, 0, len(b.Program.StatusArgs)<<1) for k, val := range b.Program.StatusArgs { if v, ok := val.(string); !ok { err = fmt.Errorf("status args is not a string for the ebpf program %s", b.Program.Name) log.Warn().Err(err).Msgf("failed to convert status args value into string for program %s", b.Program.Name) } else { args = append(args, "--"+k+" ="+v) } } prog := execCommand(cmd, args...) var out bytes.Buffer prog.Stdout = &out prog.Stderr = &out if err = prog.Run(); err != nil { log.Warn().Err(err).Msgf("l3afd : Failed to execute %s", b.Program.CmdStatus) } outStr, errStr := out.String(), out.String() if strings.EqualFold(outStr, bpfStatus) { userProgram = true } else { userProgram = false log.Warn().Msgf("bpf program not running error - %s", errStr) } } return userProgram, b.IsLoaded(), err } if len(b.Program.CmdStart) > 1 && b.Program.UserProgramDaemon { if err := b.VerifyProcessObject(); err != nil { userProgram = false log.Warn().Err(err).Msgf("process object is not created for command start - %s", b.Program.CmdStart) } else { userProgram, err = IsProcessRunning(b.Cmd.Process.Pid, b.Program.Name) if err != nil { log.Warn().Err(err).Msgf("failed to execute command status - %s user program %v", b.Program.CmdStart, userProgram) } else { userProgram = true } } } return userProgram, b.IsLoaded(), err } // VerifyAndGetArtifacts -Check binary already exists func (b *BPF) VerifyAndGetArtifacts(conf *config.Config) error { fPath := filepath.Join(conf.BPFDir, b.Program.Name, b.Program.Version, strings.Split(b.Program.Artifact, ".")[0]) if _, err := os.Stat(fPath); os.IsNotExist(err) { return b.GetArtifacts(conf) } b.FilePath = fPath return nil } // GetArtifacts downloads artifacts from the specified eBPF repo func (b *BPF) GetArtifacts(conf *config.Config) error { buf := &bytes.Buffer{} isDefaultURLUsed := false platform, err := GetPlatform() if err != nil { return fmt.Errorf("failed to identify platform type: %w", err) } RepoURL := b.Program.EPRURL if len(b.Program.EPRURL) == 0 { RepoURL = conf.EBPFRepoURL isDefaultURLUsed = true } _, err = url.Parse(RepoURL) if err != nil { if isDefaultURLUsed { return fmt.Errorf("unknown ebpf-repo format : %w", err) } else { return fmt.Errorf("unknown ebpf_package_repo_url format : %w", err) } } urlpath, err := url.JoinPath(RepoURL, b.Program.Name, b.Program.Version, platform, b.Program.Artifact) if err != nil { return fmt.Errorf("not able to join the artifact path %w", err) } log.Info().Msgf("Retrieving artifact - %s", urlpath) err = DownloadArtifact(urlpath, conf.HttpClientTimeout, buf) if err != nil { return err } tempDir := filepath.Join(conf.BPFDir, b.Program.Name, b.Program.Version) err = ExtractArtifact(b.Program.Artifact, buf, tempDir) if err != nil { return fmt.Errorf("unable to extract artifact %w", err) } newDir := strings.Split(b.Program.Artifact, ".") b.FilePath = filepath.Join(tempDir, newDir[0]) return nil } // create rules file func (b *BPF) createUpdateRulesFile(direction string) (string, error) { if len(b.Program.RulesFile) < 1 { return "", fmt.Errorf("RulesFile name is empty") } fileName := path.Join(b.FilePath, direction, b.Program.RulesFile) if err := os.WriteFile(fileName, []byte(b.Program.Rules), 0644); err != nil { return "", fmt.Errorf("create or Update Rules File failed with error %w", err) } return fileName, nil } // fileExists checks if a file exists or not func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return err == nil && !info.IsDir() } // Add eBPF map into BPFMaps list func (b *BPF) AddBPFMap(mapName string) error { bpfMap, err := b.GetBPFMap(mapName) if err != nil { return err } b.BpfMaps[mapName] = *bpfMap return nil } func (b *BPF) GetBPFMap(mapName string) (*BPFMap, error) { if b.ProgMapCollection == nil { return nil, fmt.Errorf("no handle to prog map collection") } ebpfMap, ok := b.ProgMapCollection.Maps[mapName] if !ok { return nil, fmt.Errorf("not found") } ebpfInfo, err := ebpfMap.Info() if err != nil { return nil, fmt.Errorf("fetching map info failed %w", err) } tempMapID, ok := ebpfInfo.ID() if !ok { return nil, fmt.Errorf("fetching map id failed %w", err) } newBPFMap := BPFMap{ Name: mapName, MapID: tempMapID, Type: ebpfInfo.Type, BPFProg: b, } log.Info().Msgf("added mapID %d Name %s Type %s", newBPFMap.MapID, newBPFMap.Name, newBPFMap.Type) return &newBPFMap, nil } // Add eBPF map into BPFMaps list func (b *BPF) AddMetricsBPFMap(mapName, aggregator string, key, samplesLength int) error { var tmpMetricsBPFMap MetricsBPFMap bpfMap, err := b.GetBPFMap(mapName) if err != nil { return fmt.Errorf("program %s metrics map %s not found : %w", b.Program.Name, mapName, err) } tmpMetricsBPFMap.BPFMap = *bpfMap tmpMetricsBPFMap.Key = key tmpMetricsBPFMap.Aggregator = aggregator tmpMetricsBPFMap.Values = ring.New(samplesLength) log.Info().Msgf("added Metrics map ID %d Name %s Type %s Key %d Aggregator %s", tmpMetricsBPFMap.MapID, tmpMetricsBPFMap.Name, tmpMetricsBPFMap.Type, tmpMetricsBPFMap.Key, tmpMetricsBPFMap.Aggregator) map_key := mapName + strconv.Itoa(key) + aggregator b.MetricsBpfMaps[map_key] = &tmpMetricsBPFMap return nil } // This method to fetch values from bpf maps and publish to metrics func (b *BPF) MonitorMaps(ifaceName, ipv4_address string, intervals int) error { for _, element := range b.Program.MonitorMaps { log.Debug().Msgf("monitor maps element %s key %d aggregator %s", element.Name, element.Key, element.Aggregator) mapKey := element.Name + strconv.Itoa(element.Key) + element.Aggregator _, ok := b.MetricsBpfMaps[mapKey] if !ok { if err := b.AddMetricsBPFMap(element.Name, element.Aggregator, element.Key, intervals); err != nil { return fmt.Errorf("unable to fetch map %s key %d aggregator %s : %w", element.Name, element.Key, element.Aggregator, err) } } bpfMap := b.MetricsBpfMaps[mapKey] MetricName := element.Name + "_" + strconv.Itoa(element.Key) + "_" + element.Aggregator stats.SetValue(bpfMap.GetValue(), stats.BPFMonitorMap, b.Program.Name, MetricName, ifaceName, ipv4_address) } return nil } // Updating next program FD from program ID func (b *BPF) PutNextProgFDFromID(progID int) error { if len(b.Program.MapName) == 0 { // no chaining map return nil } log.Info().Msgf("PutNextProgFDFromID : Map Name %s ID %d", b.Program.MapName, progID) ebpfMap, err := ebpf.NewMapFromID(b.ProgMapID) if err != nil { return fmt.Errorf("unable to access pinned next prog map %s %w", b.Program.MapName, err) } defer ebpfMap.Close() bpfProg, err := ebpf.NewProgramFromID(ebpf.ProgramID(progID)) if err != nil { return fmt.Errorf("failed to get next prog FD from ID for program %s %w", b.Program.Name, err) } key := 0 fd := bpfProg.FD() log.Info().Msgf("PutNextProgFDFromID : Map Name %s FD %d", b.Program.MapName, fd) if err = ebpfMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&fd), 0); err != nil { return fmt.Errorf("unable to update prog next map %s %w", b.Program.MapName, err) } return nil } // GetProgID - This returns ID of the bpf program func (b *BPF) GetProgID() (ebpf.ProgramID, error) { ebpfMap, err := ebpf.LoadPinnedMap(b.PrevMapNamePath, &ebpf.LoadPinOptions{ReadOnly: true}) if err != nil { log.Error().Err(err).Msgf("unable to access pinned prog map %s", b.PrevMapNamePath) return 0, fmt.Errorf("unable to access pinned prog map %s %w", b.PrevMapNamePath, err) } defer ebpfMap.Close() var value ebpf.ProgramID key := 0 if err = ebpfMap.Lookup(unsafe.Pointer(&key), unsafe.Pointer(&value)); err != nil { log.Warn().Err(err).Msgf("unable to look up prog map %s", b.PrevMapNamePath) return 0, fmt.Errorf("unable to look up prog map %w", err) } // verify progID before storing in locally. _, err = ebpf.NewProgramFromID(ebpf.ProgramID(value)) if err != nil { log.Warn().Err(err).Msgf("failed to verify program ID %s", b.PrevMapNamePath) return 0, fmt.Errorf("failed to verify program ID %s %w", b.Program.Name, err) } log.Info().Msgf("GetProgID - Name %s PrevMapName %s ID %d", b.Program.Name, b.PrevMapNamePath, value) return value, nil } // RemoveNextProgFD Delete the entry if its last program in the chain. // This method is called when sequence of the program changed to last in the chain func (b *BPF) RemoveNextProgFD() error { if len(b.Program.MapName) == 0 { // no chaining map in case of root programs return nil } ebpfMap, err := ebpf.NewMapFromID(b.PrevProgMapID) if err != nil { return fmt.Errorf("unable to access pinned next prog map %s %w", b.Program.MapName, err) } defer ebpfMap.Close() key := 0 if err := ebpfMap.Delete(unsafe.Pointer(&key)); err != nil { return fmt.Errorf("failed to delete prog fd entry : %w", err) } return nil } // RemovePrevProgFD Delete the entry if the last element func (b *BPF) RemovePrevProgFD() error { ebpfMap, err := ebpf.NewMapFromID(b.PrevProgMapID) if err != nil { return fmt.Errorf("unable to access pinned prev prog map %s %w", b.PrevMapNamePath, err) } defer ebpfMap.Close() key := 0 if err := ebpfMap.Delete(unsafe.Pointer(&key)); err != nil { // Some cases map may be empty ignore it. log.Debug().Err(err).Msg("RemovePrevProgFD failed") } return nil } // VerifyPinnedProgMap - making sure program fd map's pinned file is created if exists flag is true func (b *BPF) VerifyPinnedProgMap(chain, exists bool) error { if !chain { return nil } var err error if len(b.Program.MapName) > 0 { log.Debug().Msgf("VerifyPinnedMap : Program %s MapName %s Exists %t", b.Program.Name, b.Program.MapName, exists) for i := 0; i < 10; i++ { _, err = os.Stat(b.MapNamePath) if err == nil && exists { log.Info().Msgf("VerifyPinnedProgMap creation : map file created %s", b.MapNamePath) return nil } else if err != nil && !exists { if _, err = os.Stat(b.MapNamePath); os.IsNotExist(err) { log.Info().Msgf("VerifyPinnedProgMap removal : map file removed successfully - %s ", b.MapNamePath) return nil } else if err != nil { log.Warn().Err(err).Msg("VerifyPinnedProgMap removal : Error checking for map file") } else { log.Warn().Msg("VerifyPinnedProgMap removal : program pinned file still exists, checking again after a second") } } time.Sleep(1 * time.Second) } if err != nil && exists { err = fmt.Errorf("VerifyPinnedProgMap creation : failed to find pinned file %s err %w", b.MapNamePath, err) log.Error().Err(err).Msg("") } else if err != nil { err = fmt.Errorf("VerifyPinnedProgMap removal : %s map file was never removed by BPF program %s err %w", b.MapNamePath, b.Program.Name, err) log.Error().Err(err).Msg("") } return err } return nil } // VerifyProcessObject - This method to verify cmd and process object is populated or not func (b *BPF) VerifyProcessObject() error { if b.Cmd == nil { err := fmt.Errorf("command object is nil - %s", b.Program.Name) log.Error().Err(err).Msg("command object is nil -") return err } for i := 0; i < 10; i++ { if b.Cmd.Process != nil { return nil } log.Warn().Msgf("VerifyProcessObject: process object not found, checking again after a second") time.Sleep(1 * time.Second) } err := fmt.Errorf("process object is nil - %s", b.Program.Name) log.Error().Err(err).Msg("") return err } // VerifyMetricsMapsVanish - checks for all metrics maps references are removed from the kernel func (b *BPF) VerifyMetricsMapsVanish() error { for i := 0; i < 10; i++ { mapExists := false for _, v := range b.BpfMaps { _, err := ebpf.NewMapFromID(v.MapID) if err == nil { log.Warn().Msgf("VerifyMetricsMapsVanish: bpf map reference still exists - %s", v.Name) mapExists = true } } if !mapExists { return nil } log.Warn().Msgf("VerifyMetricsMapsVanish: bpf map reference still exists - %s, checking again after a second", b.Program.Name) time.Sleep(1 * time.Second) } err := fmt.Errorf("metrics maps are never removed by Kernel %s", b.Program.Name) log.Error().Err(err).Msg("") return err } // UnloadProgram - Unload or detach the program from the interface and close all the program resources func (b *BPF) UnloadProgram(ifaceName, direction string) error { // remove pinned files if err := b.RemovePinnedFiles(ifaceName); err != nil { log.Error().Err(err).Msgf("failed to remove map file for program %s => %s", ifaceName, b.Program.Name) } // Verifying program attached to the interface. // SeqID will be 0 for root program or any other program without chaining if b.Program.SeqID == 0 || !b.HostConfig.BpfChainingEnabled { if b.Program.ProgType == models.TCType { if utils.CheckTCXSupport() { if b.Link != nil { if err := b.Link.Close(); err != nil { log.Warn().Msgf("removing tc attached program %s failed iface %q direction %s error - %v", b.Program.Name, ifaceName, direction, err) } } else { log.Warn().Msgf("attach program %s link file is missing iface %q direction %s", b.Program.Name, ifaceName, direction) } } else { if err := b.UnloadTCProgram(ifaceName, direction); err != nil { log.Warn().Msgf("removing tc filter failed iface %q direction %s error - %v", ifaceName, direction, err) } } } else if b.Program.ProgType == models.XDPType { if err := b.Link.Close(); err != nil { log.Warn().Msgf("removing xdp attached program failed iface %q direction %s error - %v", ifaceName, direction, err) } } } for _, LinkObject := range b.ProbeLinks { (*LinkObject).Close() } // Release all the resources of the epbf program if b.ProgMapCollection != nil { b.ProgMapCollection.Close() } return nil } // RemovePinnedFiles - removes all the pinned files func (b *BPF) RemovePinnedFiles(ifaceName string) error { if b.ProgMapCollection != nil { for k, v := range b.ProgMapCollection.Maps { var mapFilename string version := utils.ReplaceDotsWithUnderscores(b.Program.Version) if b.Program.ProgType == models.TCType { mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, b.Program.Name, version, k) } else { mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, k) } if err := v.Unpin(); err != nil { return fmt.Errorf("BPF program %s prog type %s ifacename %s map %s:failed to pin the map err - %w", b.Program.Name, b.Program.ProgType, ifaceName, mapFilename, err) } } } // remove pinned links if b.Link != nil { if err := b.Link.Unpin(); err != nil { return fmt.Errorf("unable to unpin the xdp link for %s with err : %w", b.Program.Name, err) } } // remove programs pins if b.ProgMapCollection != nil { for _, v := range b.ProgMapCollection.Programs { if err := v.Unpin(); err != nil { return err } } } return nil } // RemoveRootProgMapFile - removes root pinned prog map file // This is invoked if any stale map file persists for root map func (b *BPF) RemoveRootProgMapFile(ifacename string) error { var mapFilename string version := utils.ReplaceDotsWithUnderscores(b.Program.Version) switch b.Program.ProgType { case models.TCType: mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, ifacename, b.Program.Name, version, b.Program.MapName) case models.XDPType: mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, ifacename, b.Program.Name, version, b.Program.MapName) default: log.Warn().Msgf("RemoveRootProgMapFile: program %s map file %s - unknown type", b.Program.Name, b.MapNamePath) return fmt.Errorf("removeMapFile: program %s unknown type %s", b.Program.Name, b.Program.ProgType) } // codeQL Check if strings.Contains(mapFilename, "..") { return fmt.Errorf("%s contains relative path is not supported - %s", mapFilename, b.Program.Name) } if err := os.Remove(mapFilename); err != nil { if !os.IsNotExist(err) { log.Warn().Msgf("RemoveRootProgMapFile: %s program type %s map file remove unsuccessfully - %s err - %#v", b.Program.ProgType, b.Program.Name, mapFilename, err) return fmt.Errorf("%s - remove failed with error %w", mapFilename, err) } } return nil } // VerifyCleanupMaps - This method verifies map entries in the fs is removed func (b *BPF) VerifyCleanupMaps(chain bool) error { // verify pinned file is removed. if err := b.VerifyPinnedProgMap(chain, false); err != nil { log.Error().Err(err).Msgf("stop user program - failed to remove pinned file %s", b.Program.Name) return fmt.Errorf("stop user program - failed to remove pinned file %s : %w", b.Program.Name, err) } // Verify all metrics map references are removed from kernel if err := b.VerifyMetricsMapsVanish(); err != nil { log.Error().Err(err).Msgf("stop user program - failed to remove metric map references %s", b.Program.Name) return fmt.Errorf("stop user program - failed to remove metric map references %s : %w", b.Program.Name, err) } return nil } // LoadBPFProgram - This method loads the eBPF program natively. func (b *BPF) LoadBPFProgram(ifaceName string) error { ObjectFile := filepath.Join(b.FilePath, b.Program.ObjectFile) if _, err := os.Stat(ObjectFile); os.IsNotExist(err) { return fmt.Errorf("%s: file doesn't exist", ObjectFile) } // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { log.Error().Msgf("failed to remove memory lock limits %#v", err) return fmt.Errorf("%s: remove rlimit lock failed : %w", b.Program.Name, err) } objSpec, err := ebpf.LoadCollectionSpec(ObjectFile) if err != nil { return fmt.Errorf("%s: loading collection spec failed - %w", ObjectFile, err) } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) if err := b.CreatePinDirectories(ifaceName, b.Program.Name, version); err != nil { return err } var mapPinPath string if b.Program.ProgType == models.TCType { mapPinPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, b.Program.Name, version) } else if b.Program.ProgType == models.XDPType { mapPinPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version) } collOptions := ebpf.CollectionOptions{ Maps: ebpf.MapOptions{ PinPath: mapPinPath, }, } // Load the BPF program with the updated map options prg, err := ebpf.NewCollectionWithOptions(objSpec, collOptions) if err != nil { return fmt.Errorf("%s: loading of bpf program failed - %w", b.Program.Name, err) } // Persist program handle b.ProgMapCollection = prg if err := b.LoadBPFProgramProbeTypes(objSpec); err != nil { return fmt.Errorf("LoadBPFProgramProbeTypes failed with error %v ", err) } //var bpfProg *ebpf.Program if len(b.Program.EntryFunctionName) > 0 { bpfProg := prg.Programs[b.Program.EntryFunctionName] if bpfProg == nil { return fmt.Errorf("%s entry function is not found in the loaded object file of the program %s", b.Program.EntryFunctionName, b.Program.Name) } progInfo, err := bpfProg.Info() if err != nil { return fmt.Errorf("%s: information of bpf program failed : %w", b.Program.Name, err) } ok := false b.ProgID, ok = progInfo.ID() if !ok { log.Warn().Msgf("Program ID fetch failed: %s", b.Program.Name) } // Initialise metric maps if err := b.InitialiseMetricMaps(); err != nil { return fmt.Errorf("initialising metric maps failed %w", err) } if err := b.PinBpfMaps(ifaceName); err != nil { return err } } return nil } // InitialiseMetricMaps - This method initialises all the monitor maps func (b *BPF) InitialiseMetricMaps() error { if b.ProgMapCollection == nil { log.Warn().Msgf("prog is not loaded by l3afd") return nil } for _, tmpMap := range b.Program.MonitorMaps { tmpMetricsMap := b.ProgMapCollection.Maps[tmpMap.Name] if tmpMetricsMap == nil { log.Error().Msgf("%s map is not loaded", tmpMap.Name) continue } var err error log.Debug().Msgf("Program - %s map name %s key size %d value size %d\n", b.Program.Name, tmpMap.Name, tmpMetricsMap.KeySize(), tmpMetricsMap.ValueSize()) if tmpMetricsMap.KeySize() == 1 { var k int8 switch tmpMetricsMap.ValueSize() { case 1: var v int8 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 2: var v int16 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 4: var v int32 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 8: var v int64 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) default: log.Error().Msgf("unsupported key type int8 and value size - %d", tmpMetricsMap.ValueSize()) } } else if tmpMetricsMap.KeySize() == 2 { var k int16 switch tmpMetricsMap.ValueSize() { case 1: var v int8 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 2: var v int16 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 4: var v int32 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 8: var v int64 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) default: log.Error().Msgf("unsupported map key type int16 and value size - %d", tmpMetricsMap.ValueSize()) } } else if tmpMetricsMap.KeySize() == 4 { var k int32 switch tmpMetricsMap.ValueSize() { case 1: var v int8 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 2: var v int16 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 4: var v int32 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 8: var v int64 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) default: log.Error().Msgf("unsupported map key type int32 and value size - %d", tmpMetricsMap.ValueSize()) } } else if tmpMetricsMap.KeySize() == 8 { var k int64 switch tmpMetricsMap.ValueSize() { case 1: var v int8 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 2: var v int16 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 4: var v int32 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) case 8: var v int64 err = tmpMetricsMap.Update(unsafe.Pointer(&k), unsafe.Pointer(&v), 0) default: log.Error().Msgf("unsupported key type int64 and value size - %d", tmpMetricsMap.ValueSize()) } } if err != nil { return fmt.Errorf("update hash map element failed for map name %s error %v", tmpMap.Name, err) } } return nil } // IsLoaded - Method verifies whether bpf program is loaded or not // Here it checks whether prog ID is valid and active func (b *BPF) IsLoaded() bool { if b.ProgID == 0 { return false } ebpfProg, err := ebpf.NewProgramFromID(b.ProgID) if err != nil { log.Debug().Msgf("IsLoaded - %s is not loaded or invalid program id %d", b.Program.Name, uint32(b.ProgID)) return false } defer ebpfProg.Close() return true } func (b *BPF) StartUserProgram(ifaceName, direction string, chain bool) error { cmd := filepath.Join(b.FilePath, b.Program.CmdStart) // Validate if err := assertExecutable(cmd); err != nil { return fmt.Errorf("no executable permissions on %s - error %w", b.Program.CmdStart, err) } args := make([]string, 0, len(b.Program.StartArgs)<<1) if len(ifaceName) > 0 { args = append(args, "--iface="+ifaceName) // attaching to interface } if len(direction) > 0 { args = append(args, "--direction="+direction) // direction xdpingress or ingress or egress } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) if len(b.Program.Version) > 0 { args = append(args, "--version="+version) // version xdpingress or ingress or egress } if chain && b.ProgMapCollection == nil { // chaining from user program if len(b.PrevMapNamePath) > 1 { args = append(args, "--map-name="+b.PrevMapNamePath) } } if len(b.HostConfig.BPFLogDir) > 1 { args = append(args, "--log-dir="+b.HostConfig.BPFLogDir) } if len(b.Program.RulesFile) > 1 && len(b.Program.Rules) > 1 { fileName, err := b.createUpdateRulesFile(direction) if err == nil { args = append(args, "--rules-file="+fileName) } } for k, val := range b.Program.StartArgs { if v, ok := val.(string); !ok { err := fmt.Errorf("start args is not a string for the bpf program %s", b.Program.Name) log.Error().Err(err).Msgf("failed to convert start args value into string for program %s", b.Program.Name) return err } else { args = append(args, "--"+k+"="+v) } } log.Info().Msgf("BPF Program start command : %s %v", cmd, args) b.Cmd = execCommand(cmd, args...) if err := b.Cmd.Start(); err != nil { log.Info().Err(err).Msgf("user program failed - %s", b.Program.Name) return fmt.Errorf("failed to start : %s %v with err: %w", cmd, args, err) } if !b.Program.UserProgramDaemon { log.Info().Msgf("no user program - %s No Pid", b.Program.Name) if err := b.Cmd.Wait(); err != nil { log.Warn().Msgf("failed at wait - %s err %s", b.Program.Name, err.Error()) } b.Cmd = nil } else { if err := b.SetPrLimits(); err != nil { log.Warn().Err(err).Msg("failed to set resource limits") } log.Info().Msgf("BPF program - %s User program Process id %d started", b.Program.Name, b.Cmd.Process.Pid) } return nil } // CreatePinDirectories - This method creates directory for ebpf objects // TC maps are pinned to directory /sys/fs/bpf/tc/globals/ // XDP maps are pinned to directory /sys/fs/bpf/ // links are pinned to directory /sys/fs/bpf/links/// // Program are pinned to directory /sys/fs/bpf/progs/// func (b *BPF) CreatePinDirectories(ifaceName, progName, progVersion string) error { var mapPathDir string if b.Program.ProgType == models.XDPType { mapPathDir = filepath.Join(b.HostConfig.BpfMapDefaultPath, ifaceName, progName, progVersion) } else if b.Program.ProgType == models.TCType { mapPathDir = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, progName, progVersion) } // Create map dir for XDP and TC programs only if len(mapPathDir) > 0 { // codeQL Check if strings.Contains(mapPathDir, "..") { return fmt.Errorf("%s contains relative path is not supported - %s", mapPathDir, b.Program.Name) } if err := os.MkdirAll(mapPathDir, 0750); err != nil { return fmt.Errorf("%s failed to create map dir path of %s program %s with err : %w", mapPathDir, b.Program.ProgType, b.Program.Name, err) } } linksPathDir := filepath.Join(b.HostConfig.BpfMapDefaultPath, "links", ifaceName, progName, progVersion) if strings.Contains(linksPathDir, "..") { return fmt.Errorf("%s contains relative path is not supported - %s", linksPathDir, b.Program.Name) } if err := os.MkdirAll(linksPathDir, 0750); err != nil { return fmt.Errorf("%s failed to create map dir path of %s program %s with err : %w", linksPathDir, b.Program.ProgType, b.Program.Name, err) } ProgPathDir := filepath.Join(b.HostConfig.BpfMapDefaultPath, "progs", ifaceName, progName, progVersion) if strings.Contains(ProgPathDir, "..") { return fmt.Errorf("%s contains relative path is not supported - %s", ProgPathDir, b.Program.Name) } if err := os.MkdirAll(ProgPathDir, 0750); err != nil { return fmt.Errorf("%s failed to create map dir path of %s program %s with err : %w", ProgPathDir, b.Program.ProgType, b.Program.Name, err) } return nil } // AttachBPFProgram - method to attach bpf program to interface func (b *BPF) AttachBPFProgram(ifaceName, direction string) error { version := utils.ReplaceDotsWithUnderscores(b.Program.Version) if b.Program.ProgType == models.XDPType { if err := b.LoadXDPAttachProgram(ifaceName); err != nil { return fmt.Errorf("failed to attach xdp program %s to inferface %s with err: %w", b.Program.Name, ifaceName, err) } // pin the program also progPinPath := utils.ProgPinPath(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, b.Program.EntryFunctionName, b.Program.ProgType) if err := b.ProgMapCollection.Programs[b.Program.EntryFunctionName].Pin(progPinPath); err != nil { return err } } else if b.Program.ProgType == models.TCType { if utils.CheckTCXSupport() { if err := b.LoadTCXAttachProgram(ifaceName, direction); err != nil { return fmt.Errorf("failed to attach tcx program %s to interface %s direction %s with err %w", b.Program.Name, ifaceName, direction, err) } } else { if err := b.LoadTCAttachProgram(ifaceName, direction); err != nil { return fmt.Errorf("failed to attach tc program %s to interface %s direction %s with err %w", b.Program.Name, ifaceName, direction, err) } } // pin the program also progPinPath := utils.ProgPinPath(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, b.Program.EntryFunctionName, b.Program.ProgType) if err := b.ProgMapCollection.Programs[b.Program.EntryFunctionName].Pin(progPinPath); err != nil { return err } } return nil } // PinBpfMaps - Pinning tc and xdp maps func (b *BPF) PinBpfMaps(ifaceName string) error { version := utils.ReplaceDotsWithUnderscores(b.Program.Version) for k, v := range b.ProgMapCollection.Maps { var mapFilename string // ebpf programs temporary storage created by eBPF program skip it if k == ".bss" { continue } if b.Program.ProgType == models.TCType { mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, ifaceName, b.Program.Name, version, k) } else { mapFilename = filepath.Join(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, k) } // In case one of the program pins the map then other program will skip if !fileExists(mapFilename) { if err := v.Pin(mapFilename); err != nil { return fmt.Errorf("eBPF program %s map %s:failed to pin the map err - %w", b.Program.Name, mapFilename, err) } } } return nil } // UpdateProgramMap - Store the program map reference func (b *BPF) UpdateProgramMap(ifaceName string) error { // Verify chaining map is provided if len(b.Program.MapName) == 0 { return fmt.Errorf("program map name is missing for %s program %s", b.Program.ProgType, b.Program.Name) } bpfRootMap := b.ProgMapCollection.Maps[b.Program.MapName] ebpfInfo, err := bpfRootMap.Info() if err != nil { return fmt.Errorf("fetching map info failed for %s program %s to interface %s : %w", b.Program.ProgType, b.Program.Name, ifaceName, err) } var ok bool b.ProgMapID, ok = ebpfInfo.ID() if !ok { return fmt.Errorf("fetching map id failed for %s program %s to interface %s : %w", b.Program.ProgType, b.Program.Name, ifaceName, err) } return nil } // LoadBPFProgramChain - Load the BPF program and chain it. func (b *BPF) LoadBPFProgramChain(ifaceName, direction string) error { if err := b.LoadBPFProgram(ifaceName); err != nil { return err } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) // pin the program also progPinPath := utils.ProgPinPath(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, b.Program.EntryFunctionName, b.Program.ProgType) if err := b.ProgMapCollection.Programs[b.Program.EntryFunctionName].Pin(progPinPath); err != nil { return err } // Update program map id if err := b.UpdateProgramMap(ifaceName); err != nil { return err } // Link this program into previous program map ebpfMap, err := ebpf.NewMapFromID(b.PrevProgMapID) if err != nil { return fmt.Errorf("unable to access pinned previous prog map %s %w", b.PrevMapNamePath, err) } defer ebpfMap.Close() bpfProg := b.ProgMapCollection.Programs[b.Program.EntryFunctionName] if bpfProg == nil { return fmt.Errorf("%s entry function is not found in the loaded object file of the program %s", b.Program.EntryFunctionName, b.Program.Name) } key := 0 fd := bpfProg.FD() log.Info().Msgf("previous program map path %s FD %d", b.PrevMapNamePath, fd) if err = ebpfMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&fd), 0); err != nil { return fmt.Errorf("unable to update prog next map %s %v", b.Program.MapName, err) } log.Info().Msgf("eBPF program %s loaded on interface %s direction %s successfully", b.Program.Name, ifaceName, direction) return nil } func (b *BPF) StopUserProgram(ifaceName, direction string) error { // Stop User Programs if any if len(b.Program.CmdStop) < 1 && b.Program.UserProgramDaemon { // Loaded using user program if err := b.ProcessTerminate(); err != nil { return fmt.Errorf("BPFProgram %s process terminate failed with error: %w", b.Program.Name, err) } if b.Cmd != nil { if err := b.Cmd.Wait(); err != nil { log.Error().Err(err).Msgf("cmd wait at stopping bpf program %s errored", b.Program.Name) } b.Cmd = nil } } else if len(b.Program.CmdStop) > 0 && b.Program.UserProgramDaemon { cmd := filepath.Join(b.FilePath, b.Program.CmdStop) if err := assertExecutable(cmd); err != nil { return fmt.Errorf("no executable permissions on %s - error %w", b.Program.CmdStop, err) } args := make([]string, 0, len(b.Program.StopArgs)<<1) if len(ifaceName) > 0 { args = append(args, "--iface="+ifaceName) // detaching from iface } if len(direction) > 0 { args = append(args, "--direction="+direction) // xdpingress or ingress or egress } for k, val := range b.Program.StopArgs { if v, ok := val.(string); !ok { err := fmt.Errorf("stop args is not a string for the bpf program %s", b.Program.Name) log.Error().Err(err).Msgf("failed to convert stop args value into string for program %s", b.Program.Name) return err } else { args = append(args, "--"+k+" ="+v) } } log.Info().Msgf("bpf user program stop command : %s %v", cmd, args) prog := execCommand(cmd, args...) if err := prog.Run(); err != nil { log.Warn().Err(err).Msgf("l3afd : Failed to stop the user program %s", b.Program.CmdStop) } b.Cmd = nil } return nil } // DownloadArtifact will download artifact from provided urlpath and store it in buffer func DownloadArtifact(urlpath string, timeout time.Duration, buf *bytes.Buffer) error { URL, err := url.Parse(urlpath) if err != nil { return fmt.Errorf("unknown url format : %w", err) } switch URL.Scheme { case models.HttpScheme, models.HttpsScheme: { timeOut := time.Duration(timeout) * time.Second var netTransport = &http.Transport{ ResponseHeaderTimeout: timeOut, } client := http.Client{Transport: netTransport, Timeout: timeOut} // Get the data resp, err := client.Get(URL.String()) if err != nil { return fmt.Errorf("download failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("get request returned unexpected status code: %d (%s), %d was expected\n\tResponse Body: %s", resp.StatusCode, http.StatusText(resp.StatusCode), http.StatusOK, buf.Bytes()) } buf.ReadFrom(resp.Body) return nil } case models.FileScheme: { if fileExists(URL.Path) { f, err := os.Open(URL.Path) if err != nil { return fmt.Errorf("opening err : %w", err) } buf.ReadFrom(f) f.Close() } else { return fmt.Errorf("artifact is not found") } return nil } default: return fmt.Errorf("unknown url scheme") } } // ExtractArtifact will extract artifact to given tempDir func ExtractArtifact(artifactName string, buf *bytes.Buffer, tempDir string) error { switch artifact := artifactName; { case strings.HasSuffix(artifact, ".zip"): { c := bytes.NewReader(buf.Bytes()) zipReader, err := zip.NewReader(c, int64(c.Len())) if err != nil { return fmt.Errorf("failed to create zip reader: %w", err) } for _, file := range zipReader.File { zippedFile, err := file.Open() if err != nil { return fmt.Errorf("unzip failed: %w", err) } defer zippedFile.Close() extractedFilePath, err := ValidatePath(file.Name, tempDir) if err != nil { return err } if file.FileInfo().IsDir() { os.MkdirAll(extractedFilePath, file.Mode()) } else { outputFile, err := os.OpenFile( extractedFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode(), ) if err != nil { return fmt.Errorf("unzip failed to create file: %w", err) } defer outputFile.Close() buf := copyBufPool.Get().(*bytes.Buffer) _, err = io.CopyBuffer(outputFile, zippedFile, buf.Bytes()) if err != nil { return fmt.Errorf("GetArtifacts failed to copy files: %w", err) } copyBufPool.Put(buf) } } return nil } case strings.HasSuffix(artifact, ".tar.gz"): { archive, err := gzip.NewReader(buf) if err != nil { return fmt.Errorf("failed to create Gzip reader: %w", err) } defer archive.Close() tarReader := tar.NewReader(archive) for { header, err := tarReader.Next() if err == io.EOF { break } else if err != nil { return fmt.Errorf("untar failed: %w", err) } fPath, err := ValidatePath(header.Name, tempDir) if err != nil { return err } info := header.FileInfo() if info.IsDir() { if err = os.MkdirAll(fPath, info.Mode()); err != nil { return fmt.Errorf("untar failed to create directories: %w", err) } continue } file, err := os.OpenFile(fPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) if err != nil { return fmt.Errorf("untar failed to create file: %w", err) } defer file.Close() buf := copyBufPool.Get().(*bytes.Buffer) _, err = io.CopyBuffer(file, tarReader, buf.Bytes()) if err != nil { return fmt.Errorf("GetArtifacts failed to copy files: %w", err) } copyBufPool.Put(buf) } return nil } default: return fmt.Errorf("unknown artifact format") } } // ValidatePath will validate any illegal file path func ValidatePath(filePath string, destination string) (string, error) { destpath := filepath.Join(destination, filePath) if strings.Contains(filePath, "..") { return "", fmt.Errorf(" file contains filepath (%s) that includes (..)", filePath) } if !strings.HasPrefix(destpath, filepath.Clean(destination)+string(os.PathSeparator)) { return "", fmt.Errorf("%s: illegal file path", filePath) } return destpath, nil } ================================================ FILE: bpfprogs/bpfCfgs_internal.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !configs // +build !configs // This file is used for walmart internal to run BPF specific configs. // We will be removing this file in future. package bpfprogs import ( "github.com/rs/zerolog/log" ) func (b *BPF) RunBPFConfigs() error { log.Warn().Msg("Implement custom BPF specific configs") return nil } ================================================ FILE: bpfprogs/bpf_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "context" "io" "net/http" "os" "os/exec" "reflect" "runtime" "strings" "testing" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/mocks" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" "go.uber.org/mock/gomock" ) var mockedExitStatus = 1 var mockPid = 77 func fakeExecCommand(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=TestHelperProcess", "--", command} cs = append(cs, args...) cmd := exec.Command(os.Args[0], cs...) cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} cmd.Process = &os.Process{Pid: mockPid} return cmd } func TestHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return } os.Exit(mockedExitStatus) } func TestNewBpfProgram(t *testing.T) { type args struct { program models.BPFProgram logDir string chain bool direction string ctx context.Context datacenter string hostConfig *config.Config } execCommand = fakeExecCommand defer func() { execCommand = exec.Command }() tests := []struct { name string args args want *BPF }{ {name: "GoodInput", args: args{ program: models.BPFProgram{ Name: "nfprogram", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", CmdConfig: "", Version: "1.0", UserProgramDaemon: true, IsPlugin: false, AdminStatus: "enabled", ProgType: "tc", }, logDir: "", chain: false, direction: "ingress", datacenter: "localdc", hostConfig: &config.Config{ BPFLogDir: "", DataCenter: "localdc", }, }, want: &BPF{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", CmdConfig: "", CmdUpdate: "", Version: "1.0", UserProgramDaemon: true, IsPlugin: false, AdminStatus: "enabled", ProgType: "tc", }, Cmd: nil, FilePath: "", BpfMaps: make(map[string]BPFMap, 0), MetricsBpfMaps: make(map[string]*MetricsBPFMap, 0), Ctx: nil, Done: nil, HostConfig: &config.Config{ BPFLogDir: "", DataCenter: "localdc", }, }, }, {name: "EmptyBPFProgram", args: args{ program: models.BPFProgram{}, hostConfig: &config.Config{}, }, want: &BPF{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", BpfMaps: make(map[string]BPFMap, 0), MetricsBpfMaps: make(map[string]*MetricsBPFMap, 0), HostConfig: &config.Config{}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewBpfProgram(tt.args.ctx, tt.args.program, tt.args.hostConfig, ifaceName); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewBpfProgram() = %#v, want %#v", got, tt.want) } }) } } func TestBPF_Stop(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int Direction string } tests := []struct { name string fields fields wantErr bool }{ {name: "NilCmd", fields: fields{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", Version: "1.0", UserProgramDaemon: true, AdminStatus: "enabled", }, Cmd: nil, FilePath: "/tmp/dummy/dummy", RestartCount: 3, }, wantErr: true, }, {name: "WithStopCmd", fields: fields{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "foo", Version: "1.0", UserProgramDaemon: true, AdminStatus: "enabled", }, Cmd: fakeExecCommand("/tmp/dummy/foo"), FilePath: "/tmp/dummy/dummy", RestartCount: 3, }, wantErr: true, }, {name: "AnyBinaryFile", fields: fields{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "data.tar.gz", CmdStart: GetTestExecutableName(), CmdStop: GetTestExecutableName(), UserProgramDaemon: false, AdminStatus: "enabled", }, Cmd: fakeExecCommand(GetTestExecutablePathName()), FilePath: GetTestExecutablePath(), RestartCount: 3, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } // Create the metric with expected labels stats.BPFRunning = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "l3afd_BPFRunning", Help: "Indicates whether eBPF program is running", }, []string{"program", "version", "direction", "iface"}, ) if err := prometheus.Register(stats.BPFRunning); err != nil { log.Warn().Err(err).Msg("Failed to register BPFRunning metrics") } // Simulate program load stats.BPFRunning.With(prometheus.Labels{ "program": b.Program.Name, "version": b.Program.Version, "direction": "ingress", "iface": "eth0", }).Set(1) stats.BPFStartTime = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "l3afd", Name: "BPFStartTime", Help: "This value indicates start time of the BPF program since unix epoch in seconds", }, []string{"host", "ebpf_program", "direction", "interface_name"}, ) if err := prometheus.Register(stats.BPFStartTime); err != nil { log.Warn().Err(err).Msg("Failed to register BPFStartTime metrics") } if err := b.Stop(ifaceName, "", models.IngressType, false); (err != nil) != tt.wantErr { t.Errorf("BPF.Stop() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestBPF_Start(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int ifaceName string hostConfig *config.Config } tests := []struct { name string fields fields wantErr bool }{ {name: "NoFilePath", fields: fields{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", RestartCount: 0, hostConfig: &config.Config{ BPFLogDir: "", }, }, wantErr: true, }, {name: "AnyBinary", fields: fields{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "data.tar.gz", CmdStart: GetTestExecutableName(), CmdStop: GetTestExecutableName(), UserProgramDaemon: true, AdminStatus: "enabled", }, Cmd: nil, FilePath: GetTestExecutablePath(), RestartCount: 0, hostConfig: &config.Config{ BPFLogDir: "", }, }, wantErr: false, }, {name: "UserProgramFalse", fields: fields{ Program: models.BPFProgram{ Name: "nfprogram", Artifact: "data.tar.gz", CmdStart: GetTestExecutableName(), CmdStop: GetTestExecutableName(), UserProgramDaemon: false, AdminStatus: "enabled", }, Cmd: fakeExecCommand(GetTestExecutablePathName()), FilePath: GetTestExecutablePath(), RestartCount: 0, hostConfig: &config.Config{ BPFLogDir: "", }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, HostConfig: tt.fields.hostConfig, } if err := b.Start(tt.fields.ifaceName, "", models.IngressType, true); (err != nil) != tt.wantErr { t.Errorf("BPF.Start() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestBPF_isRunning(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int CmdStatus string } tests := []struct { name string fields fields want bool wantErr bool }{ { name: "NoPID", fields: fields{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", RestartCount: 0, }, want: false, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } userProg, bpfProg, err := b.isRunning() if (err != nil) != tt.wantErr && (userProg == tt.want || bpfProg != tt.want) { t.Errorf("BPF.isRunning() user prog = %v, bpf prog = %v, error = %v, wantErr %v ", userProg, bpfProg, err, tt.wantErr) } }) } } // RoundTripFunc . type RoundTripFunc func(req *http.Request) *http.Response // RoundTrip . func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req), nil } // NewTestClient returns *http.Client with Transport replaced to avoid making real calls func NewTestClient(fn RoundTripFunc) *http.Client { return &http.Client{ Transport: RoundTripFunc(fn), } } func TestBPF_GetArtifacts(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int Client *http.Client } type args struct { conf *config.Config } tests := []struct { name string fields fields args args wantErr bool }{ {name: "EmptyArtifact", fields: fields{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", RestartCount: 0, }, args: args{conf: &config.Config{BPFDir: "/tmp"}}, wantErr: true, }, {name: "DummyArtifact", fields: fields{ Program: models.BPFProgram{ Name: "dummy", Version: "1", Artifact: "dummy.tar.gz", }, Cmd: nil, FilePath: "", RestartCount: 0, Client: NewTestClient(func(r *http.Request) *http.Response { buf := new(bytes.Buffer) writer := gzip.NewWriter(buf) defer writer.Close() tarWriter := tar.NewWriter(writer) defer tarWriter.Close() header := new(tar.Header) header.Name = "random" header.Mode = 0777 tarWriter.WriteHeader(header) tarWriter.Write([]byte("random things")) return &http.Response{ StatusCode: 200, Body: io.NopCloser(buf), Header: make(http.Header), } }), }, args: args{conf: &config.Config{BPFDir: "/tmp", EBPFRepoURL: "https://l3af.io/"}}, wantErr: true, }, { name: "Unknown_url_with_http_scheme", fields: fields{ Program: models.BPFProgram{ EPRURL: "http://www.example.com", }, Cmd: nil, FilePath: "", RestartCount: 0, }, args: args{ conf: &config.Config{ BPFDir: "/tmp", EBPFRepoURL: "https://l3af.io/", }, }, wantErr: true, }, { name: "Unknown_url_with_file_scheme", fields: fields{ Program: models.BPFProgram{ EPRURL: "file:///Users/random/dummy.tar.gz", }, Cmd: nil, FilePath: "", RestartCount: 0, }, args: args{ conf: &config.Config{ BPFDir: "/tmp", EBPFRepoURL: "https://l3af.io/", }, }, wantErr: true, }, { name: "Unknown_scheme", fields: fields{ Program: models.BPFProgram{ EPRURL: "ftp://ftp.foo.org/dummy.tar.gz", }, Cmd: nil, FilePath: "", RestartCount: 0, }, args: args{ conf: &config.Config{ BPFDir: "/tmp", EBPFRepoURL: "https://l3af.io/", }, }, wantErr: true, }, { name: "ZipReaderFail", fields: fields{ Program: models.BPFProgram{ Name: "testebpfprogram", Version: "1.0", Artifact: "testebpfprogram.zip", }, Cmd: nil, FilePath: "", RestartCount: 0, Client: NewTestClient(func(r *http.Request) *http.Response { buf := new(bytes.Buffer) writer := zip.NewWriter(buf) f, _ := writer.Create("testebpfprogram") data := strings.NewReader("this is just a test ebpf program") io.Copy(f, data) writer.Close() return &http.Response{ StatusCode: 200, Body: io.NopCloser(buf), Header: make(http.Header), } }), }, args: args{ conf: &config.Config{ BPFDir: "/tmp", }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } ctrl := gomock.NewController(t) defer ctrl.Finish() m := mocks.NewMockplatformInterface(ctrl) if runtime.GOOS == "windows" { m.EXPECT().GetPlatform().Return("windows", nil).AnyTimes() } else { m.EXPECT().GetPlatform().Return("focal", nil).AnyTimes() } err := b.GetArtifacts(tt.args.conf) if (err != nil) != tt.wantErr { t.Errorf("BPF.download() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestBPF_SetPrLimits(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd // Pid int FilePath string RestartCount int } tests := []struct { name string fields fields wantErr bool }{ {name: "DefaultLimitsWithNoCmd", fields: fields{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", RestartCount: 0, }, wantErr: true, }, {name: "ValidLimitsWithNoCmd", fields: fields{ Program: models.BPFProgram{ CPU: 100, Memory: 1024, }, Cmd: fakeExecCommand("/foo/foo"), FilePath: "", RestartCount: 0, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, // Pid: tt.fields.Pid, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } if err := b.SetPrLimits(); (err != nil) != tt.wantErr { t.Errorf("BPF.SetPrLimits() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_assertExecute(t *testing.T) { type args struct { filepath string } tests := []struct { name string args args wantErr bool }{ { name: "InvalidFilepath", args: args{ filepath: "/tmp/dummyfile", }, wantErr: true, }, { name: "ValidFilepath", args: args{ filepath: GetTestExecutablePathName(), }, wantErr: false, }, { name: "ValidFilepathWihoutExecute", args: args{ filepath: GetTestNonexecutablePathName(), }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := assertExecutable(tt.args.filepath); (err != nil) != tt.wantErr { t.Errorf("assertExecute() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_fileExists(t *testing.T) { tests := []struct { name string fileName string exist bool }{ { name: "invalidfilename", fileName: "blahblah", exist: false, }, } for _, tt := range tests { if fileExists(tt.fileName) != tt.exist { t.Errorf("Invalid filename") } } } func Test_StopExternalRunningProcess(t *testing.T) { tests := []struct { name string processName string wantErr bool }{ { name: "emptyProcessName", processName: "", wantErr: true, }, } for _, tt := range tests { err := StopExternalRunningProcess(tt.processName) if (err != nil) != tt.wantErr { t.Errorf("Error During execution StopExternalRunningProcess : %v", err) } } } func Test_createUpdateRulesFile(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd // Pid int FilePath string RestartCount int } tests := []struct { name string fields fields wantErr bool }{ { name: "emptyRuleFileName", fields: fields{ Program: models.BPFProgram{ RulesFile: "", }, Cmd: nil, FilePath: "", RestartCount: 0, }, wantErr: true, }, { name: "invalidPath", fields: fields{ Program: models.BPFProgram{ RulesFile: "bad", }, Cmd: nil, FilePath: "/dummy/fpp", RestartCount: 0, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } _, err := b.createUpdateRulesFile("ingress") if (err != nil) != tt.wantErr { t.Errorf("createUpdateRulesFile() error : %v", err) } }) } } func Test_PutNextProgFDFromID(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int hostConfig *config.Config } tests := []struct { name string fields fields wantErr bool progId int hostConfig *config.Config }{ { name: "emptyMapName", fields: fields{ Program: models.BPFProgram{ MapName: "", }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, wantErr: false, progId: 1, }, { name: "invalidMapName", fields: fields{ Program: models.BPFProgram{ MapName: "invalidname", }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, wantErr: true, progId: 1, }, { name: "invalidProgID", fields: fields{ Program: models.BPFProgram{ Name: "ratelimiting", SeqID: 1, Artifact: "l3af_ratelimiting.tar.gz", MapName: "xdp_rl_ingress_next_prog", CmdStart: "ratelimiting", Version: "latest", UserProgramDaemon: true, AdminStatus: "enabled", ProgType: "xdp", CfgVersion: 1, }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, progId: -1, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, HostConfig: tt.fields.hostConfig, } err := b.PutNextProgFDFromID(tt.progId) if (err != nil) != tt.wantErr { t.Errorf("PutNextProgFDFromID() error : %v", err) } }) } } func Test_VerifyPinnedProgMapExists(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int hostConfig *config.Config } tests := []struct { name string fields fields wantErr bool }{ { name: "invalidMapName", fields: fields{ Program: models.BPFProgram{ MapName: "invalid", }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, HostConfig: tt.fields.hostConfig, } err := b.VerifyPinnedProgMap(true, true) if (err != nil) != tt.wantErr { t.Errorf("VerifyPinnedMapExists() error : %v", err) } }) } } func Test_VerifyProcessObject(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int } tests := []struct { name string fields fields wantErr bool }{ { name: "nilCmd", fields: fields{ Program: models.BPFProgram{}, Cmd: nil, FilePath: "", RestartCount: 0, }, wantErr: true, }, { name: "nillCmdProcess", fields: fields{ Program: models.BPFProgram{}, Cmd: &exec.Cmd{ Process: nil, }, FilePath: "", RestartCount: 0, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, } err := b.VerifyProcessObject() if (err != nil) != tt.wantErr { t.Errorf("VerifyProcessObject() error : %v", err) } }) } } func Test_VerifyPinnedProgMapVanish(t *testing.T) { type fields struct { Program models.BPFProgram Cmd *exec.Cmd FilePath string RestartCount int hostConfig *config.Config } tests := []struct { name string fields fields wantErr bool }{ { name: "emptyMapName", fields: fields{ Program: models.BPFProgram{ MapName: "", }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, wantErr: false, }, { name: "invalidProgType", fields: fields{ Program: models.BPFProgram{ MapName: "tc/globals/something", ProgType: models.TCType, }, hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := &BPF{ Program: tt.fields.Program, Cmd: tt.fields.Cmd, FilePath: tt.fields.FilePath, RestartCount: tt.fields.RestartCount, HostConfig: &config.Config{ BpfMapDefaultPath: tt.fields.hostConfig.BpfMapDefaultPath, }, } err := b.VerifyPinnedProgMap(true, false) if (err != nil) != tt.wantErr { t.Errorf("VerifyPinnedMapVanish() error : %v", err) } }) } } ================================================ FILE: bpfprogs/bpf_test_unix.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !WINDOWS // +build !WINDOWS package bpfprogs import ( "fmt" "os" ) func GetTestNonexecutablePathName() string { return "/var/log/syslog" } func GetTestExecutablePathName() string { return "/bin/date" } func GetTestExecutablePath() string { return "/bin" } func GetTestExecutableName() string { return "date" } // assertExecutable checks for executable permissions func assertExecutable(fPath string) error { info, err := os.Stat(fPath) if err != nil { return fmt.Errorf("could not stat file: %s with error: %w", fPath, err) } if (info.Mode()&os.ModePerm)&os.FileMode(executePerm) == 0 { return fmt.Errorf("file: %s, is not executable", fPath) } return nil } ================================================ FILE: bpfprogs/bpf_test_windows.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build WINDOWS // +build WINDOWS package bpfprogs import ( "fmt" "os" "strings" ) func GetTestNonexecutablePathName() string { return "c:/windows/system32/drivers/etc/host" } func GetTestExecutablePathName() string { return "c:/windows/system32/net.exe" } func GetTestExecutablePath() string { return "c:/windows/system32" } func GetTestExecutableName() string { return "net.exe" } // assertExecutable checks for executable permissions func assertExecutable(fPath string) error { _, err := os.Stat(fPath) if err != nil { return fmt.Errorf("could not stat file: %s with error: %v", fPath, err) } // info.Mode() does not return the correct permissions on Windows, // it always has the 'x' permissions clear, so instead use the file suffix. if !strings.HasSuffix(fPath, ".exe") { return fmt.Errorf("file: %s, is not executable", fPath) } return nil } ================================================ FILE: bpfprogs/bpf_unix.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !WINDOWS // +build !WINDOWS // Package bpfprogs provides primitives for l3afd's network function configs. package bpfprogs import ( "bytes" "errors" "fmt" "net" "os" "strings" "syscall" "unsafe" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/utils" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/florianl/go-tc" "github.com/florianl/go-tc/core" "github.com/rs/zerolog/log" "github.com/safchain/ethtool" "golang.org/x/sys/unix" ) // DisableLRO - XDP programs are failing when LRO is enabled, to fix this we use to manually disable. // # ethtool -K ens7 lro off // # ethtool -k ens7 | grep large-receive-offload // large-receive-offload: off func DisableLRO(ifaceName string) error { ethHandle, err := ethtool.NewEthtool() if err != nil { err = fmt.Errorf("ethtool failed to get the handle %w", err) log.Error().Err(err).Msg("") return err } defer ethHandle.Close() config := make(map[string]bool, 1) config["rx-lro"] = false if err := ethHandle.Change(ifaceName, config); err != nil { err = fmt.Errorf("ethtool failed to disable LRO on %s with err %w", ifaceName, err) log.Error().Err(err).Msg("") return err } return nil } // prLimit set the memory and cpu limits for the bpf program func prLimit(pid int, limit uintptr, rlimit *unix.Rlimit) error { _, _, errno := unix.RawSyscall6(unix.SYS_PRLIMIT64, uintptr(pid), limit, uintptr(unsafe.Pointer(rlimit)), 0, 0, 0) if errno != 0 { log.Error().Msgf("Failed to set prlimit for process %d and errorno %d", pid, errno) return errors.New("failed to set prlimit") } return nil } // Set process resource limits only non-zero value func (b *BPF) SetPrLimits() error { var rlimit unix.Rlimit if b.Cmd == nil { return errors.New("no Process to set limits") } if b.Program.Memory != 0 { rlimit.Cur = uint64(b.Program.Memory) rlimit.Max = uint64(b.Program.Memory) if err := prLimit(b.Cmd.Process.Pid, unix.RLIMIT_AS, &rlimit); err != nil { log.Error().Err(err).Msgf("Failed to set Memory limits - %s", b.Program.Name) } } if b.Program.CPU != 0 { rlimit.Cur = uint64(b.Program.CPU) rlimit.Max = uint64(b.Program.CPU) if err := prLimit(b.Cmd.Process.Pid, unix.RLIMIT_CPU, &rlimit); err != nil { log.Error().Err(err).Msgf("Failed to set CPU limits - %s", b.Program.Name) } } return nil } // ProcessTerminate - Send sigterm to the process func (b *BPF) ProcessTerminate() error { if err := b.Cmd.Process.Signal(syscall.SIGTERM); err != nil { return fmt.Errorf("BPFProgram %s SIGTERM failed with error: %w", b.Program.Name, err) } return nil } // VerifyNMountBPFFS - Mounting bpf filesystem func VerifyNMountBPFFS() error { dstPath := "/sys/fs/bpf" srcPath := "bpffs" fstype := "bpf" flags := 0 mnts, err := os.ReadFile("/proc/mounts") if err != nil { return fmt.Errorf("failed to read procfs: %w", err) } if !strings.Contains(string(mnts), dstPath) { log.Warn().Msg("bpf filesystem is not mounted going to mount") if err = syscall.Mount(srcPath, dstPath, fstype, uintptr(flags), ""); err != nil { return fmt.Errorf("unable to mount %s at %s: %w", srcPath, dstPath, err) } } return VerifyNMountTraceFS() } // VerifyNMounTraceFS - Mounting trace filesystem func VerifyNMountTraceFS() error { dstPath := "/sys/kernel/debug/tracing" srcPath := "tracefs" fstype := "tracefs" flags := syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_RELATIME mnts, err := os.ReadFile("/proc/self/mounts") if err != nil { return fmt.Errorf("failed to read procfs: %w", err) } if !strings.Contains(string(mnts), dstPath) { log.Warn().Msgf(" %s filesystem is not mounted going to mount", dstPath) if _, err = os.Stat(dstPath); err != nil { log.Warn().Msgf(" %s directory doesn't exists, creating", dstPath) if err := os.Mkdir(dstPath, 0700); err != nil { return fmt.Errorf("unable to create mount point %s : %w", dstPath, err) } } if err = syscall.Mount(srcPath, dstPath, fstype, uintptr(flags), ""); err != nil { return fmt.Errorf("unable to mount %s at %s: %w", srcPath, dstPath, err) } } return nil } // This method get the Linux distribution Codename. This logic works on ubuntu // Here assumption is all edge nodes are running with lsb modules. // It returns empty string in case of error func GetPlatform() (string, error) { linuxDistrib := execCommand("lsb_release", "-cs") var out bytes.Buffer linuxDistrib.Stdout = &out if err := linuxDistrib.Run(); err != nil { return "", fmt.Errorf("l3afd/nf : Failed to run command with error: %w", err) } return strings.TrimSpace(out.String()), nil } func IsProcessRunning(pid int, name string) (bool, error) { procState, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) if err != nil { return false, fmt.Errorf("BPF Program not running %s because of error: %w", name, err) } var u1, u2, state string _, err = fmt.Sscanf(string(procState), "%s %s %s", &u1, &u2, &state) if err != nil { return false, fmt.Errorf("failed to scan proc state with error: %w", err) } if state == "Z" { return false, fmt.Errorf("process %d in Zombie state", pid) } return true, nil } // VerifyNCreateTCDirs - Creating BPF sudo FS for pinning TC maps func VerifyNCreateTCDirs() error { path := "/sys/fs/bpf/tc/globals" if _, err := os.Stat(path); err == nil { log.Debug().Msgf(" %s tc directory exists", path) return nil } log.Info().Msgf(" %s tc directory doesn't exists, creating", path) if err := os.MkdirAll(path, 0700); err != nil { return fmt.Errorf("unable to create directories to pin tc maps %s : %w", path, err) } return nil } // LoadTCAttachProgram - Load and attach tc root program filters or any tc program when chaining is disabled func (b *BPF) LoadTCAttachProgram(ifaceName, direction string) error { iface, err := net.InterfaceByName(ifaceName) if err != nil { log.Error().Err(err).Msgf("LoadTCAttachProgram - look up network iface %q", ifaceName) return err } if err := b.LoadBPFProgram(ifaceName); err != nil { return err } // verify and add attribute clsact tcgo, err := tc.Open(&tc.Config{}) if err != nil { return fmt.Errorf("could not open rtnetlink socket for interface %s : %w", ifaceName, err) } clsactFound := false htbFound := false ingressFound := false var htbHandle uint32 var ingressHandle uint32 var parentHandle uint32 // get all the qdiscs from all interfaces qdiscs, err := tcgo.Qdisc().Get() if err != nil { return fmt.Errorf("could not get qdiscs for interface %s : %w", ifaceName, err) } for _, qdisc := range qdiscs { iface, err := net.InterfaceByIndex(int(qdisc.Ifindex)) if err != nil { return fmt.Errorf("could not get interface %s from id %d: %w", ifaceName, qdisc.Ifindex, err) } if iface.Name == ifaceName { switch qdisc.Kind { case "clsact": clsactFound = true case "htb": htbFound = true htbHandle = qdisc.Msg.Handle case "ingress": ingressFound = true ingressHandle = qdisc.Msg.Handle default: log.Info().Msgf("Un-supported qdisc kind for interface %s : %s ", ifaceName, qdisc.Kind) } } } bpfRootProg := b.ProgMapCollection.Programs[b.Program.EntryFunctionName] var parent uint32 var filter tc.Object progFD := uint32(bpfRootProg.FD()) // Netlink attribute used in the Linux kernel bpfFlag := uint32(tc.BpfActDirect) if clsactFound { if direction == models.IngressType { parent = tc.HandleMinIngress } else if direction == models.EgressType { parent = tc.HandleMinEgress } filter = tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0, Parent: core.BuildHandle(tc.HandleRoot, parent), Info: 0x300, }, Attribute: tc.Attribute{ Kind: "bpf", BPF: &tc.Bpf{ FD: &progFD, Flags: &bpfFlag, }, }, } } else if !clsactFound && !ingressFound && !htbFound { qdisc := tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: core.BuildHandle(tc.HandleRoot, 0x0000), Parent: tc.HandleIngress, Info: 0, }, Attribute: tc.Attribute{ Kind: "clsact", }, } if err := tcgo.Qdisc().Add(&qdisc); err != nil { log.Info().Msgf("could not assign clsact to %s : %v, its already exists", ifaceName, err) } if direction == models.IngressType { parent = tc.HandleMinIngress } else if direction == models.EgressType { parent = tc.HandleMinEgress } filter = tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0, Parent: core.BuildHandle(tc.HandleRoot, parent), Info: 0x300, }, Attribute: tc.Attribute{ Kind: "bpf", BPF: &tc.Bpf{ FD: &progFD, Flags: &bpfFlag, }, }, } } else if !clsactFound && ingressFound && htbFound { if direction == models.IngressType { parentHandle = ingressHandle } else if direction == models.EgressType { parentHandle = htbHandle } // parentNew needs to handle of HTB and ingress 1:, and ffff: filter = tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0, Parent: parentHandle, Info: 0x300, }, Attribute: tc.Attribute{ Kind: "bpf", BPF: &tc.Bpf{ FD: &progFD, Flags: &bpfFlag, }, }, } } else { log.Info().Msgf("Unable to create qdisc object for interface %s", ifaceName) } // Storing Filter handle filterHandle := tcgo.Filter() // Attaching / Adding as filter if err := filterHandle.Add(&filter); err != nil { return fmt.Errorf("could not attach filter to interface %s for eBPF program %s : %w", ifaceName, b.Program.Name, err) } if b.HostConfig.BpfChainingEnabled { if err = b.UpdateProgramMap(ifaceName); err != nil { return err } } return nil } // UnloadTCProgram - Remove TC filters func (b *BPF) UnloadTCProgram(ifaceName, direction string) error { iface, err := net.InterfaceByName(ifaceName) if err != nil { log.Error().Err(err).Msgf("UnloadTCProgram - look up network iface %q", ifaceName) return err } tcgo, err := tc.Open(&tc.Config{}) if err != nil { log.Error().Err(err).Msgf("UnloadTCProgram - Unable to tc.Open(&tc.Config{}): %q", ifaceName) return err } clsactFound := false htbFound := false ingressFound := false var htbHandle uint32 var ingressHandle uint32 var parentHandle uint32 // get all the qdiscs from all interfaces qdiscs, err := tcgo.Qdisc().Get() if err != nil { log.Warn().Msgf("Could not get filters for interface \"%s\" direction %s ", ifaceName, direction) return fmt.Errorf("could not get filters for interface %s : %w", ifaceName, err) } for _, qdisc := range qdiscs { iface, err := net.InterfaceByIndex(int(qdisc.Ifindex)) if err != nil { return fmt.Errorf("could not get interface %s from id %d: %v", ifaceName, qdisc.Ifindex, err) } if iface.Name == ifaceName { switch qdisc.Kind { case "clsact": clsactFound = true case "htb": htbFound = true htbHandle = qdisc.Msg.Handle case "ingress": ingressFound = true ingressHandle = qdisc.Msg.Handle default: log.Info().Msgf("qdisc kind for %s : %v", ifaceName, qdisc.Kind) } } } bpfRootProg := b.ProgMapCollection.Programs[b.Program.EntryFunctionName] // Storing Filter handle filterHandle := tcgo.Filter() var parent uint32 var filter tc.Object if clsactFound && !ingressFound && !htbFound { if direction == models.IngressType { parent = tc.HandleMinIngress } else if direction == models.EgressType { parent = tc.HandleMinEgress } tcfilts, err := filterHandle.Get(&tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0x0, Parent: core.BuildHandle(tc.HandleRoot, parent), }) if err != nil { log.Warn().Msgf("Could not get filters for interface \"%s\" direction %s ", ifaceName, direction) return fmt.Errorf("could not get filters for interface %s : %v", ifaceName, err) } progFD := uint32(bpfRootProg.FD()) // Netlink attribute used in the Linux kernel bpfFlag := uint32(tc.BpfActDirect) if len(tcfilts) > 0 { filter = tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0, Parent: core.BuildHandle(tc.HandleRoot, parent), Info: tcfilts[0].Msg.Info, }, Attribute: tc.Attribute{ Kind: "bpf", BPF: &tc.Bpf{ FD: &progFD, Flags: &bpfFlag, }, }, } } else { log.Warn().Msgf("unload TC program clasct filters not found for interface %s direction %s", ifaceName, direction) } } else if !clsactFound && ingressFound && htbFound { if direction == models.EgressType { parentHandle = htbHandle // _ = pa("parentNew...1 ", parentNew) } else if direction == models.IngressType { parentHandle = ingressHandle } tcfilts, err := filterHandle.Get(&tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0x0, Parent: parentHandle, }) if err != nil { log.Warn().Msgf("Could not get filters for interface \"%s\" direction %s ", ifaceName, direction) return fmt.Errorf("could not get filters for interface %s : %v", ifaceName, err) } progFD := uint32(bpfRootProg.FD()) // Netlink attribute used in the Linux kernel bpfFlag := uint32(tc.BpfActDirect) var tcFilterIndex int for i, tcfilt := range tcfilts { // finding the Info field of the relevant BPF filter among all set filters for that qdisc if tcfilt.Attribute.Kind == "bpf" { tcFilterIndex = i } } // Add a check for if tcFilterIndex out of bounds if len(tcfilts) > 0 { filter = tc.Object{ Msg: tc.Msg{ Family: unix.AF_UNSPEC, Ifindex: uint32(iface.Index), Handle: 0, Parent: parentHandle, Info: tcfilts[tcFilterIndex].Msg.Info, }, Attribute: tc.Attribute{ Kind: "bpf", BPF: &tc.Bpf{ FD: &progFD, Flags: &bpfFlag, }, }, } } else { log.Warn().Msgf("unload TC program ingress or htp filters not found for interface %s direction %s", ifaceName, direction) } } // Detaching / Deleting filter if err := filterHandle.Delete(&filter); err != nil { return fmt.Errorf("could not dettach tc filter for interface %s : Direction: %v, parentHandle: %v, Error:%w", ifaceName, direction, parentHandle, err) } return nil } // LoadTCXAttachProgram - Load and attach xdp root program or any xdp program when chaining is disabled func (b *BPF) LoadTCXAttachProgram(ifaceName, direction string) error { iface, err := net.InterfaceByName(ifaceName) if err != nil { log.Error().Err(err).Msgf("LoadXDPAttachProgram -look up network iface %q", ifaceName) return err } if err := b.LoadBPFProgram(ifaceName); err != nil { return err } var attachType ebpf.AttachType if direction == models.IngressType { attachType = ebpf.AttachTCXIngress } else { attachType = ebpf.AttachTCXEgress } b.Link, err = link.AttachTCX(link.TCXOptions{ Program: b.ProgMapCollection.Programs[b.Program.EntryFunctionName], Interface: iface.Index, Attach: attachType, }) if err != nil { return fmt.Errorf("could not attach tc program %s to interface %s direction %s : %w", b.Program.Name, ifaceName, direction, err) } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) // Pin the Link linkPinPath := utils.TCLinkPinPath(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, b.Program.ProgType, direction) if err := b.Link.Pin(linkPinPath); err != nil { return fmt.Errorf("tcx program pinning failed program %s direction %s interface %s : %w", b.Program.Name, direction, ifaceName, err) } if b.HostConfig.BpfChainingEnabled { if err = b.UpdateProgramMap(ifaceName); err != nil { return err } } return nil } // LoadXDPAttachProgram - Load and attach xdp root program or any xdp program when chaining is disabled // This method has been moved to linux specific till cilium windows library implements link.AttachXDP func (b *BPF) LoadXDPAttachProgram(ifaceName string) error { iface, err := net.InterfaceByName(ifaceName) if err != nil { log.Error().Err(err).Msgf("LoadXDPAttachProgram -look up network iface %q", ifaceName) return err } if err := b.LoadBPFProgram(ifaceName); err != nil { return err } b.Link, err = link.AttachXDP(link.XDPOptions{ Program: b.ProgMapCollection.Programs[b.Program.EntryFunctionName], Interface: iface.Index, }) if err != nil { return fmt.Errorf("could not attach xdp program %s to interface %s : %w", b.Program.Name, ifaceName, err) } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) // Pin the Link linkPinPath := utils.LinkPinPath(b.HostConfig.BpfMapDefaultPath, ifaceName, b.Program.Name, version, b.Program.ProgType) if err := b.Link.Pin(linkPinPath); err != nil { return fmt.Errorf("xdp program pinning failed program %s interface %s : %w", b.Program.Name, ifaceName, err) } if b.HostConfig.BpfChainingEnabled { if err = b.UpdateProgramMap(ifaceName); err != nil { return err } } return nil } ================================================ FILE: bpfprogs/bpf_windows.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build WINDOWS // +build WINDOWS // Package bpfprogs provides primitives for l3afd's network function configs. package bpfprogs import ( "errors" "fmt" "os" "github.com/cilium/ebpf" ) // DisableLRO - XDP programs are failing when Large Receive Offload is enabled, to fix this we use to manually disable. func DisableLRO(ifaceName string) error { return nil } // Set process resource limits only non-zero value func (b *BPF) SetPrLimits() error { if b.Cmd == nil { return errors.New("no Process to set limits") } return nil } // VerifyNMountBPFFS - Mounting bpf filesystem func VerifyNMountBPFFS() error { return nil } func GetPlatform() (string, error) { return "Windows", nil } func IsProcessRunning(pid int, name string) (bool, error) { _, err := os.FindProcess(pid) if err != nil { return false, fmt.Errorf("BPF Program not running %s because of error: %w", name, err) } return true, nil } // ProcessTerminate - Kills the process func (b *BPF) ProcessTerminate() error { if err := b.Cmd.Process.Kill(); err != nil { return fmt.Errorf("BPFProgram %s kill failed with error: %w", b.Program.Name, err) } return nil } // VerifyNCreateTCDirs - Creating BPF sudo FS for pinning TC maps func VerifyNCreateTCDirs() error { return nil } // LoadTCAttachProgram - not implemented in windows func (b *BPF) LoadTCAttachProgram(ifaceName, direction string) error { // not implement nothing todo return fmt.Errorf("LoadTCAttachProgram - TC programs Unsupported on windows") } // LoadTCXAttachProgram - not implemented in windows func (b *BPF) LoadTCXAttachProgram(ifaceName, direction string) error { // not implement nothing todo return fmt.Errorf("LoadTCXAttachProgram - TC programs Unsupported on windows") } // UnloadTCProgram - Remove TC filters func (b *BPF) UnloadTCProgram(ifaceName, direction string) error { // not implement nothing todo return fmt.Errorf("UnloadTCProgram - TC programs Unsupported on windows") } // LoadXDPAttachProgram - Attaches XDP program to interface func (b *BPF) LoadXDPAttachProgram(ifaceName string) error { // not implement nothing todo return fmt.Errorf("LoadXDPAttachProgram - AttachXDP method is Unsupported on windows") } // LoadBPFProgramProbeType - Loads Probe type bpf program func (b *BPF) LoadBPFProgramProbeTypes(objSpec *ebpf.CollectionSpec) error { // not implement nothing todo return fmt.Errorf("LoadBPFProgramProbeTypes - Probes are Unsupported on windows") } ================================================ FILE: bpfprogs/bpfdebug.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "encoding/json" "errors" "net" "net/http" "strings" "github.com/l3af-project/l3afd/v2/models" "github.com/rs/zerolog/log" ) var bpfcfgs *NFConfigs func SetupBPFDebug(ebpfChainDebugAddr string, BPFConfigs *NFConfigs) { bpfcfgs = BPFConfigs go func() { if _, ok := models.AllNetListeners.Load("debug_http"); !ok { tcpAddr, err := net.ResolveTCPAddr("tcp", ebpfChainDebugAddr) if err != nil { log.Fatal().Err(err).Msgf("unable to resolve tcpaddr %v ", ebpfChainDebugAddr) return } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { log.Fatal().Err(err).Msgf("unable to create tcp listener") } models.AllNetListeners.Store("debug_http", listener) } http.HandleFunc("/bpfs/", ViewHandler) // We just need to start a server. log.Info().Msg("Starting BPF debug server") val, _ := models.AllNetListeners.Load("debug_http") l, _ := val.(*net.TCPListener) if err := http.Serve(l, nil); !errors.Is(err, http.ErrServerClosed) { log.Fatal().Err(err).Msg("failed to start BPF chain debug server") } }() } func ViewHandler(w http.ResponseWriter, r *http.Request) { iface := strings.TrimPrefix(r.URL.Path, "/bpfs/") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(bpfcfgs.BPFDetails(iface)); err != nil { log.Err(err).Msgf("unable to serialize json") } } ================================================ FILE: bpfprogs/bpfmap.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "container/ring" "errors" "fmt" "math" "unsafe" "github.com/cilium/ebpf" "github.com/l3af-project/l3afd/v2/models" "github.com/rs/zerolog/log" ) type BPFMap struct { Name string MapID ebpf.MapID Type ebpf.MapType // BPFProg reference in case of stale map id BPFProg *BPF `json:"-"` } // This stores Metrics map details. type MetricsBPFMap struct { BPFMap Key int Values *ring.Ring Aggregator string LastValue float64 } // The RemoveMissingKeys function is used to delete missing entries of eBPF maps, which are used by eBPF Programs. func (b *BPFMap) RemoveMissingKeys(args []models.KeyValue) error { ebpfMap, err := ebpf.NewMapFromID(b.MapID) if err != nil { return fmt.Errorf("access new map from ID failed %w", err) } defer ebpfMap.Close() KeyValueMap := make(map[int]bool, len(args)) for _, k := range args { KeyValueMap[k.Key] = true } var key, nextKey int for { err := ebpfMap.NextKey(unsafe.Pointer(&key), unsafe.Pointer(&nextKey)) if err != nil { if errors.Is(err, ebpf.ErrKeyNotExist) { break } else { return fmt.Errorf("get next key failed with error %w, mapid %d", err, b.MapID) } } key = nextKey _, IsKeyExists := KeyValueMap[key] if !IsKeyExists { log.Info().Msgf("removing key %v because it is missing\n", key) if err := ebpfMap.Delete(unsafe.Pointer(&key)); err != nil { return fmt.Errorf("delete key failed with error %w, mapid %d", err, b.MapID) } } } return nil } // The update function is used to update eBPF maps, which are used by eBPF programs. func (b *BPFMap) Update(key, value int) error { log.Debug().Msgf("update map name %s ID %d", b.Name, b.MapID) ebpfMap, err := ebpf.NewMapFromID(b.MapID) if err != nil { return fmt.Errorf("access new map from ID failed %w", err) } defer ebpfMap.Close() log.Info().Msgf("updating map %s key %d mapid %d", b.Name, key, b.MapID) if err := ebpfMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), 0); err != nil { return fmt.Errorf("update hash map element failed for key %d error %w", key, err) } return nil } // Get value of the map for given key // There are 2 aggregators are supported here // max-rate - this calculates delta requests / sec and stores absolute value. // avg - stores the values in the circular queue // We can implement more aggregate function as needed. func (b *MetricsBPFMap) GetValue() float64 { ebpfMap, err := ebpf.NewMapFromID(b.MapID) if err != nil { // We have observed in smaller configuration VM's, if we restart BPF's // Stale mapID's are reported, in such cases re-checking map id log.Warn().Err(err).Msgf("GetValue : NewMapFromID failed ID %d, re-looking up of map id", b.MapID) tmpBPF, err := b.BPFProg.GetBPFMap(b.Name) if err != nil { log.Warn().Err(err).Msgf("GetValue: Update new map ID %d", tmpBPF.MapID) return 0 } log.Info().Msgf("GetValue: Update new map ID %d", tmpBPF.MapID) b.MapID = tmpBPF.MapID ebpfMap, err = ebpf.NewMapFromID(b.MapID) if err != nil { log.Warn().Err(err).Msgf("GetValue : retry of NewMapFromID failed ID %d", b.MapID) return 0 } } defer ebpfMap.Close() var value int64 if err = ebpfMap.Lookup(unsafe.Pointer(&b.Key), unsafe.Pointer(&value)); err != nil { log.Warn().Err(err).Msgf("GetValue Lookup failed : Name %s ID %d", b.Name, b.MapID) return 0 } var retVal float64 switch b.Aggregator { case "scalar": retVal = float64(value) case "max-rate": b.Values = b.Values.Next() b.Values.Value = math.Abs(float64(float64(value) - b.LastValue)) b.LastValue = float64(value) retVal = b.MaxValue() case "avg": b.Values.Value = value b.Values = b.Values.Next() retVal = b.AvgValue() default: log.Warn().Msgf("unsupported aggregator %s and value %d", b.Aggregator, value) } return retVal } // This method finds the max value in the circular list func (b *MetricsBPFMap) MaxValue() float64 { tmp := b.Values var max float64 for i := 0; i < b.Values.Len(); i++ { if tmp.Value != nil { val := tmp.Value.(float64) if max < val { max = val } } tmp = tmp.Next() } return max } // This method calculates the average func (b *MetricsBPFMap) AvgValue() float64 { tmp := b.Values.Next() var sum float64 var n float64 = 0.0 for i := 0; i < b.Values.Len(); i++ { if tmp.Value != nil { sum = sum + tmp.Value.(float64) n = n + 1 } tmp = tmp.Next() } return sum / n } ================================================ FILE: bpfprogs/bpfmap_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "container/ring" "reflect" "testing" ) var TestValues *ring.Ring = ring.New(10) func SetupTestValues() { a := [10]float64{8, 10, 6, 23, 4, 53, 32, 8, 2, 7} v := TestValues for i := 0; i < TestValues.Len(); i++ { v.Value = a[i] v = v.Next() } } func TestMetricsBPFMapMaxValue(t *testing.T) { type args struct { key int Values *ring.Ring aggregator string } SetupTestValues() tests := []struct { name string args args want float64 wantErr bool }{ { name: "max-rate", args: args{key: 0, Values: TestValues, aggregator: "max-rate"}, want: 53, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { metricsMap := &MetricsBPFMap{ Values: TestValues, Key: 0, Aggregator: tt.args.aggregator, LastValue: 0, } got := (metricsMap.MaxValue()) if !reflect.DeepEqual(got, tt.want) { t.Errorf("MaxValue() = %v, want %v", got, tt.want) } }) } } func TestMetricsBPFMapAvgValue(t *testing.T) { type args struct { key int Values *ring.Ring aggregator string } SetupTestValues() tests := []struct { name string args args want float64 wantErr bool }{ { name: "avg", args: args{key: 0, Values: TestValues, aggregator: "avg"}, want: 15.3, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { metricsMap := &MetricsBPFMap{ Values: TestValues, Key: 0, Aggregator: tt.args.aggregator, LastValue: 0, } got := (metricsMap.AvgValue()) if !reflect.DeepEqual(got, tt.want) { t.Errorf("AvgValue() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: bpfprogs/bpfmetrics.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package bpfprogs provides primitives for NF process monitoring. package bpfprogs import ( "container/list" "time" "github.com/l3af-project/l3afd/v2/models" "github.com/rs/zerolog/log" ) type BpfMetrics struct { Chain bool Intervals int } func NewpBpfMetrics(chain bool, interval int) *BpfMetrics { m := &BpfMetrics{ Chain: chain, Intervals: interval, } return m } func (c *BpfMetrics) BpfMetricsStart(xdpProgs, ingressTCProgs, egressTCProgs map[string]*list.List, probes *list.List, ifaces *map[string]string) { go c.BpfMetricsWorker(xdpProgs, ifaces) go c.BpfMetricsWorker(ingressTCProgs, ifaces) go c.BpfMetricsWorker(egressTCProgs, ifaces) go c.BpfMetricsProbeWorker(probes) } func (c *BpfMetrics) BpfMetricsWorker(bpfProgs map[string]*list.List, ifaces *map[string]string) { for range time.NewTicker(1 * time.Second).C { for ifaceName, bpfList := range bpfProgs { if bpfList == nil { // no bpf programs are running continue } for e := bpfList.Front(); e != nil; e = e.Next() { bpf := e.Value.(*BPF) if c.Chain && bpf.Program.SeqID == 0 { // do not monitor root program continue } if bpf.Program.AdminStatus == models.Disabled { continue } if err := bpf.MonitorMaps(ifaceName, (*ifaces)[ifaceName], c.Intervals); err != nil { log.Debug().Err(err).Msgf("pMonitor monitor maps failed - %s", bpf.Program.Name) } } } } } func (c *BpfMetrics) BpfMetricsProbeWorker(bpfProgs *list.List) { for range time.NewTicker(1 * time.Second).C { if bpfProgs == nil { time.Sleep(time.Second) continue } for e := bpfProgs.Front(); e != nil; e = e.Next() { bpf := e.Value.(*BPF) if bpf.Program.AdminStatus == models.Disabled { continue } if err := bpf.MonitorMaps("", "", c.Intervals); err != nil { log.Debug().Err(err).Msgf("pMonitor probe monitor maps failed - %s", bpf.Program.Name) } } } } ================================================ FILE: bpfprogs/bpfmetrics_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "container/list" "reflect" "testing" ) func TestNewpKFMetrics(t *testing.T) { type args struct { chain bool interval int } tests := []struct { name string args args want *BpfMetrics wantErr bool }{ { name: "EmptypCheck", args: args{chain: false, interval: 0}, want: &BpfMetrics{Chain: false, Intervals: 0}, wantErr: false, }, { name: "ValidpCheck", args: args{chain: true, interval: 10}, want: &BpfMetrics{Chain: true, Intervals: 10}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := NewpBpfMetrics(tt.args.chain, tt.args.interval) if !reflect.DeepEqual(got, tt.want) { t.Errorf("NewKFMetrics() = %v, want %v", got, tt.want) } }) } } func Test_BPFMetrics_Start(t *testing.T) { type fields struct { Chain bool Interval int } type args struct { IngressXDPbpfProgs map[string]*list.List IngressTCbpfProgs map[string]*list.List EgressTCbpfProgs map[string]*list.List Probes *list.List Ifaces map[string]string } tests := []struct { name string fields fields args args wantErr bool }{ { name: "EmptyBPF", fields: fields{Chain: true, Interval: 10}, args: args{IngressXDPbpfProgs: make(map[string]*list.List), IngressTCbpfProgs: make(map[string]*list.List), EgressTCbpfProgs: make(map[string]*list.List), Ifaces: map[string]string{}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &BpfMetrics{ Chain: tt.fields.Chain, Intervals: tt.fields.Interval, } c.BpfMetricsStart(tt.args.IngressXDPbpfProgs, tt.args.IngressTCbpfProgs, tt.args.EgressTCbpfProgs, tt.args.Probes, &tt.args.Ifaces) }) } } ================================================ FILE: bpfprogs/nfconfig.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package bpfprogs provides primitives for l3afd's network function configs. package bpfprogs import ( "container/list" "context" "encoding/json" "errors" "fmt" "net" "os" "reflect" "slices" "sort" "strings" "sync" "time" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" ) type NFConfigs struct { Ctx context.Context HostName string HostInterfaces map[string]bool // configs sync.Map // key: string, val: *models.L3afDNFConfigDetail // These holds bpf programs in the list // map keys are network iface names index's are seq_id, position in the chain // root element will be root program IngressXDPBpfs map[string]*list.List IngressTCBpfs map[string]*list.List EgressTCBpfs map[string]*list.List ProbesBpfs list.List HostConfig *config.Config ProcessMon *PCheck BpfMetricsMon *BpfMetrics // keep track of interfaces Ifaces map[string]string Mu *sync.Mutex } func NewNFConfigs(ctx context.Context, host string, hostConf *config.Config, pMon *PCheck, metricsMon *BpfMetrics) (*NFConfigs, error) { nfConfigs := &NFConfigs{ Ctx: ctx, HostName: host, HostConfig: hostConf, IngressXDPBpfs: make(map[string]*list.List), IngressTCBpfs: make(map[string]*list.List), EgressTCBpfs: make(map[string]*list.List), Mu: new(sync.Mutex), Ifaces: make(map[string]string), } var err error if nfConfigs.HostInterfaces, err = getHostInterfaces(); err != nil { errOut := fmt.Errorf("%s failed to get network interfaces %w", host, err) log.Error().Err(errOut) return nil, errOut } nfConfigs.ProcessMon = pMon nfConfigs.ProcessMon.PCheckStart(nfConfigs.IngressXDPBpfs, nfConfigs.IngressTCBpfs, nfConfigs.EgressTCBpfs, &nfConfigs.ProbesBpfs, &nfConfigs.Ifaces) nfConfigs.BpfMetricsMon = metricsMon nfConfigs.BpfMetricsMon.BpfMetricsStart(nfConfigs.IngressXDPBpfs, nfConfigs.IngressTCBpfs, nfConfigs.EgressTCBpfs, &nfConfigs.ProbesBpfs, &nfConfigs.Ifaces) return nfConfigs, nil } // Close stop all the eBPF Programs and delete elements in the list func (c *NFConfigs) Close(ctx context.Context) error { ticker := time.NewTicker(c.HostConfig.ShutdownTimeout) defer ticker.Stop() doneCh := make(chan struct{}) var wg sync.WaitGroup // wait for waitGroup to shut down go func() { wg.Wait() close(doneCh) }() wg.Add(1) go func() { defer wg.Done() for ifaceName := range c.IngressXDPBpfs { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.XDPIngressType); err != nil { log.Warn().Err(err).Msg("failed to Close Ingress XDP BPF Program") } delete(c.IngressXDPBpfs, ifaceName) } }() wg.Add(1) go func() { defer wg.Done() for ifaceName := range c.IngressTCBpfs { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.IngressType); err != nil { log.Warn().Err(err).Msg("failed to Close Ingress TC BPF Program") } delete(c.IngressTCBpfs, ifaceName) } }() wg.Add(1) go func() { defer wg.Done() for ifaceName := range c.EgressTCBpfs { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.EgressType); err != nil { log.Warn().Err(err).Msg("failed to Close Egress TC BPF Program") } delete(c.EgressTCBpfs, ifaceName) } }() wg.Add(1) go func() { defer wg.Done() if err := c.StopNRemoveAllBPFProbePrograms(); err != nil { log.Warn().Err(err).Msg("failed to Close Probe BPF Programs") } }() select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: // didn't close successfully return fmt.Errorf("nfconfig close didn't got processed in shutdownInterval ms %v", c.HostConfig.ShutdownTimeout) case <-doneCh: // we deleted successfully } return nil } // Check for XDP programs are not loaded then initialise the array // Check for XDP root program is running for a interface. if not loaded it func (c *NFConfigs) VerifyAndStartXDPRootProgram(ifaceName, direction string) error { if err := DisableLRO(ifaceName); err != nil { return fmt.Errorf("failed to disable lro %w", err) } if err := VerifyNMountBPFFS(); err != nil { return fmt.Errorf("failed to mount bpf file system with err %w", err) } // chaining is disabled nothing to do if !c.HostConfig.BpfChainingEnabled { return nil } if c.IngressXDPBpfs[ifaceName].Len() == 0 { rootBpf, err := LoadRootProgram(ifaceName, direction, models.XDPType, c.HostConfig) if err != nil { return fmt.Errorf("failed to load %s xdp root program: %w", direction, err) } log.Info().Msg("ingress xdp root program attached") c.IngressXDPBpfs[ifaceName].PushFront(rootBpf) } return nil } // Check for TC root program is running for a interface. If not start it func (c *NFConfigs) VerifyAndStartTCRootProgram(ifaceName, direction string) error { if err := VerifyNMountBPFFS(); err != nil { return fmt.Errorf("failed to mount bpf file system with err : %w", err) } if err := VerifyNCreateTCDirs(); err != nil { return fmt.Errorf("failed to create tc/global diretories with err: %w", err) } // Check for chaining flag if !c.HostConfig.BpfChainingEnabled { return nil } if direction == models.IngressType { if c.IngressTCBpfs[ifaceName].Len() == 0 { //Root program is not running start then rootBpf, err := LoadRootProgram(ifaceName, direction, models.TCType, c.HostConfig) if err != nil { return fmt.Errorf("failed to load %s tc root program: %w", direction, err) } log.Info().Msg("ingress tc root program attached") c.IngressTCBpfs[ifaceName].PushFront(rootBpf) } } else { if c.EgressTCBpfs[ifaceName].Len() == 0 { //Root program is not running start then rootBpf, err := LoadRootProgram(ifaceName, direction, models.TCType, c.HostConfig) if err != nil { return fmt.Errorf("failed to load %s tc root program: %w", direction, err) } log.Info().Msg("egress tc root program attached") c.EgressTCBpfs[ifaceName].PushFront(rootBpf) } } return nil } // This method inserts the element at the end of the list func (c *NFConfigs) PushBackAndStartBPF(bpfProg *models.BPFProgram, ifaceName, direction string) error { log.Info().Msgf("PushBackAndStartBPF : iface %s, direction %s", ifaceName, direction) bpf := NewBpfProgram(c.Ctx, *bpfProg, c.HostConfig, ifaceName) var bpfList *list.List switch direction { case models.XDPIngressType: bpfList = c.IngressXDPBpfs[ifaceName] case models.IngressType: bpfList = c.IngressTCBpfs[ifaceName] case models.EgressType: bpfList = c.EgressTCBpfs[ifaceName] default: // we should never reach here return fmt.Errorf("unknown direction type") } if err := c.DownloadAndStartBPFProgram(bpfList.PushBack(bpf), ifaceName, direction); err != nil { return fmt.Errorf("failed to download and start the BPF %s iface %s direction %s with err: %w", bpfProg.Name, ifaceName, direction, err) } return nil } func (c *NFConfigs) DownloadAndStartBPFProgram(element *list.Element, ifaceName, direction string) error { if element == nil { return fmt.Errorf("element is nil pointer") } bpf := element.Value.(*BPF) if element.Prev() != nil { prevBPF := element.Prev().Value.(*BPF) bpf.PrevMapNamePath = prevBPF.MapNamePath bpf.PrevProgMapID = prevBPF.ProgMapID log.Info().Msgf("DownloadAndStartBPFProgram : program name %s previous program map name: %s", bpf.Program.Name, bpf.PrevMapNamePath) } if err := bpf.VerifyAndGetArtifacts(c.HostConfig); err != nil { return fmt.Errorf("failed to get artifacts %s with error: %w", bpf.Program.Artifact, err) } if err := bpf.Start(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to start bpf program %s with error: %w", bpf.Program.Name, err) } return nil } // Stopping all programs in order func (c *NFConfigs) StopNRemoveAllBPFPrograms(ifaceName, direction string) error { var bpfList *list.List switch direction { case models.XDPIngressType: bpfList = c.IngressXDPBpfs[ifaceName] c.IngressXDPBpfs[ifaceName] = nil case models.IngressType: bpfList = c.IngressTCBpfs[ifaceName] c.IngressTCBpfs[ifaceName] = nil case models.EgressType: bpfList = c.EgressTCBpfs[ifaceName] c.EgressTCBpfs[ifaceName] = nil default: // we should never reach here return fmt.Errorf("unknown direction type %s", direction) } if bpfList == nil { log.Warn().Msgf("no %s ebpf programs to stop", direction) return nil } for e := bpfList.Front(); e != nil; { data := e.Value.(*BPF) if err := data.Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop program %s direction %s with err :%w", data.Program.Name, direction, err) } nextBPF := e.Next() bpfList.Remove(e) e = nextBPF } return nil } // StopNRemoveAllBPFProbePrograms Stopping all probe programs in order func (c *NFConfigs) StopNRemoveAllBPFProbePrograms() error { for e := c.ProbesBpfs.Front(); e != nil; { data := e.Value.(*BPF) if err := data.Stop("", "", "", false); err != nil { return fmt.Errorf("failed to stop probe program %s with err :%w", data.Program.Name, err) } nextBPF := e.Next() c.ProbesBpfs.Remove(e) e = nextBPF } return nil } // VerifyNUpdateBPFProgram - This method checks the following conditions // 1. BPF Program already running with no change // 2. BPF Program running but needs to stop (admin_status == disabled) // 3. BPF Program running but needs version update // 4. BPF Program running but position change (seq_id change) // 5. BPF Program not running but needs to start. // 6. BPF Program running but map args change, will update the map values (i.e. Array and Hash maps only) // 7. BPF Program running but update args change, will invoke cmd_update with additional option --cmd=update func (c *NFConfigs) VerifyNUpdateBPFProgram(bpfProg *models.BPFProgram, ifaceName, direction string) error { var bpfList *list.List if bpfProg == nil { return nil } switch direction { case models.XDPIngressType: bpfList = c.IngressXDPBpfs[ifaceName] case models.IngressType: bpfList = c.IngressTCBpfs[ifaceName] case models.EgressType: bpfList = c.EgressTCBpfs[ifaceName] default: return fmt.Errorf("unknown direction type") } for e := bpfList.Front(); e != nil; e = e.Next() { data := e.Value.(*BPF) if strings.Compare(data.Program.Name, bpfProg.Name) != 0 { continue } if reflect.DeepEqual(data.Program, *bpfProg) { // Nothing to do return nil } // Admin status change - disabled if data.Program.AdminStatus != bpfProg.AdminStatus { log.Info().Msgf("verifyNUpdateBPFProgram :admin_status change detected - disabling the program %s", data.Program.Name) data.Program.AdminStatus = bpfProg.AdminStatus if err := data.Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop to on admin_status change BPF %s iface %s direction %s admin_status %s with err %w", bpfProg.Name, ifaceName, direction, bpfProg.AdminStatus, err) } tmpNextBPF := e.Next() tmpPreviousBPF := e.Prev() bpfList.Remove(e) if tmpNextBPF != nil && tmpNextBPF.Prev() != nil { // relink the next element if err := c.LinkBPFPrograms(tmpNextBPF.Prev().Value.(*BPF), tmpNextBPF.Value.(*BPF)); err != nil { log.Error().Err(err).Msg("admin status disabled - failed LinkBPFPrograms") return fmt.Errorf("admin status disabled - failed LinkBPFPrograms %w", err) } } // if chaining is disabled prev will be nil if tmpPreviousBPF == nil && tmpNextBPF == nil { switch direction { case models.XDPIngressType: c.IngressXDPBpfs[ifaceName] = nil case models.IngressType: c.IngressTCBpfs[ifaceName] = nil case models.EgressType: c.EgressTCBpfs[ifaceName] = nil default: return fmt.Errorf("unknown direction type %s", direction) } return nil } // Check if list contains root program only then stop the root program. if tmpPreviousBPF.Prev() == nil && tmpPreviousBPF.Next() == nil { log.Info().Msg("no eBPF Programs are running, stopping root program") if c.HostConfig.BpfChainingEnabled { if err := c.StopRootProgram(ifaceName, direction); err != nil { return fmt.Errorf("failed to stop to root program %s iface %s direction %s with err: %w", bpfProg.Name, ifaceName, direction, err) } } } return nil } // Version Change if data.Program.Version != bpfProg.Version || !reflect.DeepEqual(data.Program.StartArgs, bpfProg.StartArgs) { log.Info().Msgf("VerifyNUpdateBPFProgram : version update initiated - current version %s new version %s", data.Program.Version, bpfProg.Version) if err := data.Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop older version of network function BPF %s iface %s direction %s version %s with err: %w", bpfProg.Name, ifaceName, direction, bpfProg.Version, err) } data.Program = *bpfProg if err := c.DownloadAndStartBPFProgram(e, ifaceName, direction); err != nil { return fmt.Errorf("failed to download and start newer version of network function BPF %s version %s iface %s direction %s with err: %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } // update if not a last program if e.Next() != nil { data.PutNextProgFDFromID(int(e.Next().Value.(*BPF).ProgID)) } return nil } // monitor maps change if !reflect.DeepEqual(data.Program.MonitorMaps, bpfProg.MonitorMaps) { log.Info().Msgf("monitor map list is mismatch - updated") data.Program.MonitorMaps = bpfProg.MonitorMaps } // Update CfgVersion data.Program.CfgVersion = bpfProg.CfgVersion // Seq ID Change if data.Program.SeqID != bpfProg.SeqID { log.Info().Msgf("VerifyNUpdateBPFProgram : seq id change detected %s current seq id %d new seq id %d", data.Program.Name, data.Program.SeqID, bpfProg.SeqID) // Update seq id data.Program.SeqID = bpfProg.SeqID if err := c.MoveToLocation(e, bpfList); err != nil { return fmt.Errorf("failed to move to new position in the chain BPF %s version %s iface %s direction %s with err: %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } } // map arguments change - basically any config change to ebpf program updating config maps if !reflect.DeepEqual(data.Program.MapArgs, bpfProg.MapArgs) { log.Info().Msg("maps_args are mismatched") data.Program.MapArgs = bpfProg.MapArgs data.UpdateBPFMaps(ifaceName, c.Ifaces[ifaceName], direction) } // update arguments change - basically any config change to ebpf program config maps using user program if !reflect.DeepEqual(data.Program.UpdateArgs, bpfProg.UpdateArgs) { log.Info().Msg("update_args are mismatched") data.Program.UpdateArgs = bpfProg.UpdateArgs data.UpdateArgs(ifaceName, c.Ifaces[ifaceName], direction) } return nil } log.Debug().Msgf("Program is not found in the list name %s", bpfProg.Name) // if not found in the list. if err := c.InsertAndStartBPFProgram(bpfProg, ifaceName, direction); err != nil { return fmt.Errorf("failed to insert and start BPFProgram to new location BPF %s version %s iface %s direction %s with err: %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } return nil } func (c *NFConfigs) MoveToLocation(element *list.Element, bpfList *list.List) error { if element == nil { return fmt.Errorf("MoveToLocation - element is nil") } bpf := element.Value.(*BPF) if bpfList == nil { log.Warn().Msg("ebpf program list is empty") return nil } for e := bpfList.Front(); e != nil; e = e.Next() { data := e.Value.(*BPF) if data.Program.SeqID >= bpf.Program.SeqID && data.Program.Name != bpf.Program.Name { if element.Next() != nil && element.Prev() != nil { if err := c.LinkBPFPrograms(element.Prev().Value.(*BPF), element.Next().Value.(*BPF)); err != nil { log.Error().Err(err).Msg("MoveToLocation - failed LinkBPFPrograms before move") return fmt.Errorf("MoveToLocation - failed LinkBPFPrograms before move %w", err) } } else if element.Next() == nil && element.Prev() != nil { if err := element.Prev().Value.(*BPF).RemoveNextProgFD(); err != nil { log.Error().Err(err).Msg("failed to remove program fd in map") return fmt.Errorf("failed to remove program fd in map %w", err) } } bpfList.MoveBefore(element, e) if err := c.LinkBPFPrograms(element.Prev().Value.(*BPF), element.Value.(*BPF)); err != nil { log.Error().Err(err).Msg("MoveToLocation - failed LinkBPFPrograms after move element to with prev prog") return fmt.Errorf("MoveToLocation - failed LinkBPFPrograms after move element to with prev prog %w", err) } if element.Next() != nil { if err := c.LinkBPFPrograms(element.Value.(*BPF), element.Next().Value.(*BPF)); err != nil { log.Error().Err(err).Msg("MoveToLocation - failed LinkBPFPrograms after move element to with next prog") return fmt.Errorf("MoveToLocation - failed LinkBPFPrograms after move element to with next prog %w", err) } } log.Info().Msgf("MoveToLocation : Moved - %s", element.Value.(*BPF).Program.Name) return nil } } log.Info().Msg("element seq id greater than last element in the list move to back of the list") if element.Next() != nil && element.Prev() != nil { if err := c.LinkBPFPrograms(element.Prev().Value.(*BPF), element.Next().Value.(*BPF)); err != nil { log.Error().Err(err).Msg("MoveToLocation - failed LinkBPFPrograms before MoveToBack element to with prev prog") return fmt.Errorf("MoveToLocation - failed LinkBPFPrograms before MoveToBack element to with prev prog %w", err) } } bpfList.MoveToBack(element) if element.Prev() != nil { if err := c.LinkBPFPrograms(element.Prev().Value.(*BPF), element.Value.(*BPF)); err != nil { log.Error().Err(err).Msg("MoveToLocation - failed LinkBPFPrograms after MoveToBack element to with prev prog") return fmt.Errorf("MoveToLocation - failed LinkBPFPrograms after MoveToBack element to with prev prog %w", err) } } if element.Next() == nil { if err := element.Value.(*BPF).RemoveNextProgFD(); err != nil { log.Error().Err(err).Msg("failed to remove MoveToBack program fd in map") return fmt.Errorf("failed to remove MoveToBack program fd in map %w", err) } } log.Info().Msgf("MoveToLocation : MoveToBack Moved - %s", element.Value.(*BPF).Program.Name) return nil } // InsertAndStartBPFProgram method for tc programs func (c *NFConfigs) InsertAndStartBPFProgram(bpfProg *models.BPFProgram, ifaceName, direction string) error { var bpfList *list.List if bpfProg == nil { return fmt.Errorf("InsertAndStartBPFProgram - bpf program is nil") } if bpfProg.AdminStatus == models.Disabled { return nil } bpf := NewBpfProgram(c.Ctx, *bpfProg, c.HostConfig, ifaceName) switch direction { case models.XDPIngressType: bpfList = c.IngressXDPBpfs[ifaceName] case models.IngressType: bpfList = c.IngressTCBpfs[ifaceName] case models.EgressType: bpfList = c.EgressTCBpfs[ifaceName] default: return fmt.Errorf("unknown direction type") } if bpfList == nil { log.Warn().Msgf("%s program list is empty", direction) return nil } for e := bpfList.Front(); e != nil; e = e.Next() { data := e.Value.(*BPF) if data.Program.SeqID >= bpfProg.SeqID { tmpBPF := bpfList.InsertBefore(bpf, e) if err := c.DownloadAndStartBPFProgram(tmpBPF, ifaceName, direction); err != nil { return fmt.Errorf("failed to download and start network function %s version %s iface %s direction %s with err %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } if tmpBPF.Next() != nil { if err := c.LinkBPFPrograms(tmpBPF.Value.(*BPF), tmpBPF.Next().Value.(*BPF)); err != nil { log.Error().Err(err).Msg("InsertAndStartBPFProgram - failed LinkBPFPrograms after InsertBefore element to with next prog") return fmt.Errorf("InsertAndStartBPFProgram - failed LinkBPFPrograms after InsertBefore element to with next prog %w", err) } } return nil } } // insert at the end if err := c.PushBackAndStartBPF(bpfProg, ifaceName, direction); err != nil { return fmt.Errorf("failed to push back and start network function %s version %s iface %s direction %s with err: %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } return nil } // StopRootProgram -This method stops the root program, removes the root node from the list and reset the list to nil func (c *NFConfigs) StopRootProgram(ifaceName, direction string) error { switch direction { case models.XDPIngressType: if c.IngressXDPBpfs[ifaceName] == nil { log.Warn().Msg("xdp root program is not running") return nil } if err := c.IngressXDPBpfs[ifaceName].Front().Value.(*BPF).Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop xdp root program iface %s with err %w", ifaceName, err) } c.IngressXDPBpfs[ifaceName].Remove(c.IngressXDPBpfs[ifaceName].Front()) c.IngressXDPBpfs[ifaceName] = nil case models.IngressType: if c.IngressTCBpfs[ifaceName] == nil { log.Warn().Msgf("tc root program %s not running", direction) return nil } if err := c.IngressTCBpfs[ifaceName].Front().Value.(*BPF).Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop ingress tc root program on interface %s with err %w", ifaceName, err) } c.IngressTCBpfs[ifaceName].Remove(c.IngressTCBpfs[ifaceName].Front()) c.IngressTCBpfs[ifaceName] = nil case models.EgressType: if c.EgressTCBpfs[ifaceName] == nil { log.Warn().Msgf("tc root program %s not running", direction) return nil } if err := c.EgressTCBpfs[ifaceName].Front().Value.(*BPF).Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop egress tc root program on interface %s with err %w", ifaceName, err) } c.EgressTCBpfs[ifaceName].Remove(c.EgressTCBpfs[ifaceName].Front()) c.EgressTCBpfs[ifaceName] = nil default: return fmt.Errorf("unknown direction type") } return nil } // Link BPF programs func (c *NFConfigs) LinkBPFPrograms(leftBPF, rightBPF *BPF) error { log.Info().Msgf("LinkBPFPrograms : left BPF Prog %s right BPF Prog %s", leftBPF.Program.Name, rightBPF.Program.Name) rightBPF.PrevMapNamePath = leftBPF.MapNamePath rightBPF.PrevProgMapID = leftBPF.PrevProgMapID if err := leftBPF.PutNextProgFDFromID(int(rightBPF.ProgID)); err != nil { log.Error().Err(err).Msgf("LinkBPFPrograms - failed to update program fd in prev prog map before move") return fmt.Errorf("LinkBPFPrograms - failed to update program fd in prev prog prog map before move %w", err) } return nil } // BPFDetails - Method provides dump of BPFs for debug purpose func (c *NFConfigs) BPFDetails(iface string) []*BPF { arrBPFDetails := make([]*BPF, 0) bpfList := c.IngressXDPBpfs[iface] if bpfList != nil { for e := bpfList.Front(); e != nil; e = e.Next() { arrBPFDetails = append(arrBPFDetails, e.Value.(*BPF)) } } bpfList = c.IngressTCBpfs[iface] if bpfList != nil { for e := bpfList.Front(); e != nil; e = e.Next() { arrBPFDetails = append(arrBPFDetails, e.Value.(*BPF)) } } bpfList = c.EgressTCBpfs[iface] if bpfList != nil { for e := bpfList.Front(); e != nil; e = e.Next() { arrBPFDetails = append(arrBPFDetails, e.Value.(*BPF)) } } for e := c.ProbesBpfs.Front(); e != nil; e = e.Next() { arrBPFDetails = append(arrBPFDetails, e.Value.(*BPF)) } return arrBPFDetails } func (c *NFConfigs) Deploy(ifaceName, HostName string, bpfProgs *models.BPFPrograms) error { if HostName != c.HostName { errOut := fmt.Errorf("provided bpf programs do not belong to this host") log.Error().Err(errOut) stats.Add(1, stats.BPFDeployFailedCount, "", "", ifaceName, c.Ifaces[ifaceName]) return errOut } if ifaceName == "" || bpfProgs == nil { errOut := fmt.Errorf("iface name or bpf programs are empty") log.Error().Err(errOut) stats.Add(1, stats.BPFDeployFailedCount, "", "", ifaceName, c.Ifaces[ifaceName]) return errOut } c.HostInterfaces, _ = getHostInterfaces() if _, ok := c.HostInterfaces[ifaceName]; !ok { c.CleanupProgramsOnInterface(ifaceName) errOut := fmt.Errorf("%s interface name not found in the host Stop called", ifaceName) stats.Add(1, stats.BPFDeployFailedCount, "", "", ifaceName, c.Ifaces[ifaceName]) log.Error().Err(errOut) return errOut } c.Mu.Lock() defer c.Mu.Unlock() for _, bpfProg := range bpfProgs.XDPIngress { if c.IngressXDPBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.IngressXDPBpfs[ifaceName] = list.New() if err := c.VerifyAndStartXDPRootProgram(ifaceName, models.XDPIngressType); err != nil { c.IngressXDPBpfs[ifaceName] = nil stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.XDPType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to chain XDP BPF programs: %w", err) } log.Info().Msgf("Push Back and Start XDP program : %s seq_id : %d", bpfProg.Name, bpfProg.SeqID) if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.XDPIngressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.XDPType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update BPF Program: %w", err) } } } else if err := c.VerifyNUpdateBPFProgram(bpfProg, ifaceName, models.XDPIngressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.XDPType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update xdp BPF Program: %w", err) } } for _, bpfProg := range bpfProgs.TCIngress { if c.IngressTCBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.IngressTCBpfs[ifaceName] = list.New() if err := c.VerifyAndStartTCRootProgram(ifaceName, models.IngressType); err != nil { c.IngressTCBpfs[ifaceName] = nil stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.IngressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to chain ingress tc bpf programs: %w", err) } if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.IngressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.IngressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update BPF Program: %w", err) } } } else if err := c.VerifyNUpdateBPFProgram(bpfProg, ifaceName, models.IngressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.IngressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update BPF Program: %w", err) } } for _, bpfProg := range bpfProgs.TCEgress { if c.EgressTCBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.EgressTCBpfs[ifaceName] = list.New() if err := c.VerifyAndStartTCRootProgram(ifaceName, models.EgressType); err != nil { c.EgressTCBpfs[ifaceName] = nil stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.EgressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to chain ingress tc bpf programs: %w", err) } if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.EgressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.EgressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update BPF Program: %w", err) } } } else if err := c.VerifyNUpdateBPFProgram(bpfProg, ifaceName, models.EgressType); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, models.EgressType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update BPF Program: %w", err) } } for _, bpfProg := range bpfProgs.Probes { if err := c.PushBackAndStartProbe(bpfProg); err != nil { stats.Add(1, stats.BPFDeployFailedCount, bpfProg.Name, bpfProg.ProgType, ifaceName, c.Ifaces[ifaceName]) return fmt.Errorf("failed to update Probe BPF Program: %w", err) } } return nil } // DeployeBPFPrograms - Starts eBPF programs on the node if they are not running func (c *NFConfigs) DeployeBPFPrograms(bpfProgs []models.L3afBPFPrograms) error { var combinedError error for _, bpfProg := range bpfProgs { if len(c.Ifaces) == 0 { c.Ifaces = map[string]string{bpfProg.Iface: bpfProg.IPv4Address} } else { c.Ifaces[bpfProg.Iface] = bpfProg.IPv4Address } if err := c.Deploy(bpfProg.Iface, bpfProg.HostName, bpfProg.BpfPrograms); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("failed to deploy BPF program on iface %s with error: %w", bpfProg.Iface, err)) } } if err := c.RemoveMissingNetIfacesNBPFProgsInConfig(bpfProgs); err != nil { log.Warn().Err(err).Msgf("Remove missing interfaces and BPF programs in the config failed with error ") } if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("deploy eBPF Programs failed to save configs %w", err)) } return combinedError } // SaveConfigsToConfigStore - Writes configs to persistent store func (c *NFConfigs) SaveConfigsToConfigStore() error { // StoreFile name is not defined then skip it if len(c.HostConfig.L3afConfigStoreFileName) < 1 { return nil } var bpfProgs []models.L3afBPFPrograms c.HostInterfaces, _ = getHostInterfaces() for iface := range c.Ifaces { if _, interfaceFound := c.HostInterfaces[iface]; interfaceFound { log.Info().Msgf("SaveConfigsToConfigStore - %s", iface) bpfPrograms := c.EBPFPrograms(iface) bpfProgs = append(bpfProgs, bpfPrograms) } } file, err := json.MarshalIndent(bpfProgs, "", " ") if err != nil { log.Error().Err(err).Msgf("failed to marshal configs to save") return fmt.Errorf("failed to marshal configs %w", err) } if err = os.WriteFile(c.HostConfig.L3afConfigStoreFileName, file, 0644); err != nil { log.Error().Err(err).Msgf("failed write to file operation") return fmt.Errorf("failed to save configs %w", err) } return nil } // EBPFPrograms - Method provides list of eBPF Programs running on iface func (c *NFConfigs) EBPFPrograms(iface string) models.L3afBPFPrograms { BPFProgram := models.L3afBPFPrograms{ HostName: c.HostName, Iface: iface, IPv4Address: c.Ifaces[iface], BpfPrograms: &models.BPFPrograms{}, } bpfList := c.IngressXDPBpfs[iface] if bpfList != nil { e := bpfList.Front() if c.HostConfig.BpfChainingEnabled && e.Value.(*BPF).Program.Name == c.HostConfig.XDPRootPackageName { e = e.Next() } for ; e != nil; e = e.Next() { BPFProgram.BpfPrograms.XDPIngress = append(BPFProgram.BpfPrograms.XDPIngress, &e.Value.(*BPF).Program) } } bpfList = c.IngressTCBpfs[iface] if bpfList != nil { e := bpfList.Front() if c.HostConfig.BpfChainingEnabled && e.Value.(*BPF).Program.Name == c.HostConfig.TCRootPackageName { e = e.Next() } for ; e != nil; e = e.Next() { BPFProgram.BpfPrograms.TCIngress = append(BPFProgram.BpfPrograms.TCIngress, &e.Value.(*BPF).Program) } } bpfList = c.EgressTCBpfs[iface] if bpfList != nil { e := bpfList.Front() if c.HostConfig.BpfChainingEnabled && e.Value.(*BPF).Program.Name == c.HostConfig.TCRootPackageName { e = e.Next() } for ; e != nil; e = e.Next() { BPFProgram.BpfPrograms.TCEgress = append(BPFProgram.BpfPrograms.TCEgress, &e.Value.(*BPF).Program) } } e := c.ProbesBpfs.Front() for ; e != nil; e = e.Next() { BPFProgram.BpfPrograms.Probes = append(BPFProgram.BpfPrograms.Probes, &e.Value.(*BPF).Program) } return BPFProgram } // EBPFProgramsAll - Method provides list of eBPF Programs running on all ifaces on the host func (c *NFConfigs) EBPFProgramsAll() []models.L3afBPFPrograms { BPFPrograms := make([]models.L3afBPFPrograms, 0) for iface := range c.Ifaces { BPFProgram := c.EBPFPrograms(iface) BPFPrograms = append(BPFPrograms, BPFProgram) } return BPFPrograms } // RemoveMissingNetIfacesNBPFProgsInConfig - Stops running eBPF programs which are missing in the config func (c *NFConfigs) RemoveMissingNetIfacesNBPFProgsInConfig(bpfProgCfgs []models.L3afBPFPrograms) error { tempIfaces := map[string]bool{} wg := sync.WaitGroup{} for _, bpfProg := range bpfProgCfgs { tempIfaces[bpfProg.Iface] = true if ifaceName, ok := c.Ifaces[bpfProg.Iface]; ok { _, ok := c.IngressXDPBpfs[ifaceName] if ok { wg.Add(1) go func(bpfProg models.L3afBPFPrograms) { defer wg.Done() if err := c.RemoveMissingBPFProgramsInConfig(bpfProg, ifaceName, models.XDPIngressType); err != nil { log.Error().Err(err).Msgf("Failed to stop missing program for network interface %s direction Ingress", ifaceName) } }(bpfProg) } _, ok = c.IngressTCBpfs[ifaceName] if ok { wg.Add(1) go func(bpfProg models.L3afBPFPrograms) { defer wg.Done() if err := c.RemoveMissingBPFProgramsInConfig(bpfProg, ifaceName, models.IngressType); err != nil { log.Error().Err(err).Msgf("Failed to stop missing program for network interface %s direction Ingress", ifaceName) } }(bpfProg) } _, ok = c.EgressTCBpfs[ifaceName] if ok { wg.Add(1) go func(bpfProg models.L3afBPFPrograms) { defer wg.Done() if err := c.RemoveMissingBPFProgramsInConfig(bpfProg, ifaceName, models.EgressType); err != nil { log.Error().Err(err).Msgf("Failed to stop missing program for network interface %s direction Ingress", ifaceName) } }(bpfProg) } } } wg.Wait() for ifaceName := range c.Ifaces { if _, ok := tempIfaces[ifaceName]; !ok { log.Info().Msgf("Missing Network Interface %s in the configs, stopping", ifaceName) if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.XDPIngressType); err != nil { log.Error().Err(err).Msgf("Failed to stop all the program in the direction xdp ingress for interface %s", ifaceName) } if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.IngressType); err != nil { log.Error().Err(err).Msgf("Failed to stop all the program in the direction tc ingress for interface %s", ifaceName) } if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.EgressType); err != nil { log.Error().Err(err).Msgf("Failed to stop all the program in the direction tc egress for interface %s", ifaceName) } delete(c.Ifaces, ifaceName) } } return nil } // RemoveMissingBPFProgramsInConfig - This method to stop the eBPF programs which are not listed in the config. func (c *NFConfigs) RemoveMissingBPFProgramsInConfig(bpfProg models.L3afBPFPrograms, ifaceName, direction string) error { var bpfProgArr []*models.BPFProgram var bpfList *list.List switch direction { case models.XDPIngressType: bpfProgArr = bpfProg.BpfPrograms.XDPIngress bpfList = c.IngressXDPBpfs[ifaceName] case models.IngressType: bpfProgArr = bpfProg.BpfPrograms.TCIngress bpfList = c.IngressTCBpfs[ifaceName] case models.EgressType: bpfProgArr = bpfProg.BpfPrograms.TCEgress bpfList = c.EgressTCBpfs[ifaceName] default: // we should never reach here return fmt.Errorf("unknown direction type %s", direction) } if bpfList == nil { // Empty list, Nothing to check return return nil } e := bpfList.Front() if e != nil && c.HostConfig.BpfChainingEnabled { e = e.Next() } for ; e != nil; e = e.Next() { prog := e.Value.(*BPF) Found := false for _, bpfConfigProg := range bpfProgArr { if bpfConfigProg.Name == prog.Program.Name { Found = true break } } if !Found { log.Info().Msgf("eBPF Program not found in config stopping - %s direction %s", prog.Program.Name, direction) prog.Program.AdminStatus = models.Disabled if err := prog.Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop to on removed config BPF %s iface %s direction %s with err %w", prog.Program.Name, ifaceName, models.XDPIngressType, err) } tmpNextBPF := e.Next() tmpPreviousBPF := e.Prev() bpfList.Remove(e) if tmpNextBPF != nil && tmpNextBPF.Prev() != nil { // relink the next element if err := c.LinkBPFPrograms(tmpNextBPF.Prev().Value.(*BPF), tmpNextBPF.Value.(*BPF)); err != nil { log.Error().Err(err).Msgf("missing config - failed LinkBPFPrograms") return fmt.Errorf("missing config - failed LinkBPFPrograms %w", err) } } // Check if list contains root program only then stop the root program. if tmpPreviousBPF.Prev() == nil && tmpPreviousBPF.Next() == nil { log.Info().Msgf("no eBPF Programs are running, stopping root program") if err := c.StopRootProgram(ifaceName, direction); err != nil { return fmt.Errorf("failed to stop to root program of iface %s direction XDP Ingress with err %w", ifaceName, err) } } } } return nil } // getHostInterfaces - return host network interfaces func getHostInterfaces() (map[string]bool, error) { var hostIfaces = make(map[string]bool, 0) ifaces, err := net.Interfaces() if err != nil { return nil, fmt.Errorf("failed to get net interfaces: %w", err) } for _, iface := range ifaces { if iface.Flags&net.FlagLoopback != 0 { continue // loopback interface } hostIfaces[iface.Name] = true } return hostIfaces, nil } func (c *NFConfigs) AddAndStartBPF(bpfProg *models.BPFProgram, ifaceName string, direction string) error { var bpfList *list.List if bpfProg == nil { return fmt.Errorf("AddAndStartBPF - bpf program is nil") } if bpfProg.AdminStatus == models.Disabled { return nil } switch direction { case models.XDPIngressType: bpfList = c.IngressXDPBpfs[ifaceName] case models.IngressType: bpfList = c.IngressTCBpfs[ifaceName] case models.EgressType: bpfList = c.EgressTCBpfs[ifaceName] default: return fmt.Errorf("unknown direction type") } for e := bpfList.Front(); e != nil; e = e.Next() { data := e.Value.(*BPF) if data.Program.Name == bpfProg.Name { log.Warn().Msgf("%v is already running on %v iface and in %v direction ", data.Program.Name, ifaceName, direction) return nil } if data.Program.SeqID == bpfProg.SeqID { log.Warn().Msgf("duplicate seq Id detected for %v in direction %v", data.Program.Name, direction) return nil } } for e := bpfList.Front(); e != nil; e = e.Next() { data := e.Value.(*BPF) if data.Program.SeqID > bpfProg.SeqID { bpf := NewBpfProgram(c.Ctx, *bpfProg, c.HostConfig, ifaceName) tmpBPF := bpfList.InsertBefore(bpf, e) if err := c.DownloadAndStartBPFProgram(tmpBPF, ifaceName, direction); err != nil { return fmt.Errorf("failed to download and start eBPF program %s version %s iface %s direction %s with err %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } if tmpBPF.Next() != nil { if err := c.LinkBPFPrograms(tmpBPF.Value.(*BPF), tmpBPF.Next().Value.(*BPF)); err != nil { log.Error().Err(err).Msg("AddAndStartBPF - failed LinkBPFPrograms after InsertBefore element to with next prog") return fmt.Errorf("AddAndStartBPFProg - failed LinkBPFPrograms after InsertBefore element to with next prog %w", err) } } return nil } } // insert at the end if err := c.PushBackAndStartBPF(bpfProg, ifaceName, direction); err != nil { return fmt.Errorf("failed to push back and start eBPF Program %s version %s iface %s direction %s with err %w", bpfProg.Name, bpfProg.Version, ifaceName, direction, err) } return nil } // AddProgramWithoutChaining : add eBPF program on given interface when chaining is not enabled func (c *NFConfigs) AddProgramWithoutChaining(ifaceName string, bpfProgs *models.BPFPrograms) error { if c.HostConfig.BpfChainingEnabled { return nil } if len(bpfProgs.XDPIngress) > 1 || len(bpfProgs.TCIngress) > 1 || len(bpfProgs.TCEgress) > 1 { return fmt.Errorf("failed to add multiple programs because chaining is disabled") } if len(bpfProgs.XDPIngress) == 1 { bpfProg := bpfProgs.XDPIngress[0] if bpfProg.AdminStatus == models.Enabled { if c.IngressXDPBpfs[ifaceName] == nil { c.IngressXDPBpfs[ifaceName] = list.New() if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.XDPIngressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } else { prog := c.IngressXDPBpfs[ifaceName].Front().Value.(*BPF) return fmt.Errorf("failed to add %v due to existing program %v on iface %v direction %v", bpfProg.Name, prog.Program.Name, ifaceName, models.XDPIngressType) } } } if len(bpfProgs.TCIngress) == 1 { bpfProg := bpfProgs.TCIngress[0] if bpfProg.AdminStatus == models.Enabled { if c.IngressTCBpfs[ifaceName] == nil { c.IngressTCBpfs[ifaceName] = list.New() if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.IngressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } else { prog := c.IngressTCBpfs[ifaceName].Front().Value.(*BPF) return fmt.Errorf("failed to add %v due to existing program %v on iface %v direction %v", bpfProg.Name, prog.Program.Name, ifaceName, models.IngressType) } } } if len(bpfProgs.TCEgress) == 1 { bpfProg := bpfProgs.TCEgress[0] if bpfProg.AdminStatus == models.Enabled { if c.EgressTCBpfs[ifaceName] == nil { c.EgressTCBpfs[ifaceName] = list.New() if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.EgressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } else { prog := c.EgressTCBpfs[ifaceName].Front().Value.(*BPF) return fmt.Errorf("failed to add %v due to existing program %v on iface %v direction %v", bpfProg.Name, prog.Program.Name, ifaceName, models.EgressType) } } } return nil } // AddProgramsOnInterface will add given ebpf programs on given interface func (c *NFConfigs) AddProgramsOnInterface(ifaceName, HostName string, bpfProgs *models.BPFPrograms) error { if HostName != c.HostName { errOut := fmt.Errorf("provided bpf programs do not belong to this host") log.Error().Err(errOut) return errOut } if ifaceName == "" || bpfProgs == nil { errOut := fmt.Errorf("iface name or bpf programs are empty") log.Error().Err(errOut) return errOut } c.HostInterfaces, _ = getHostInterfaces() if _, ok := c.HostInterfaces[ifaceName]; !ok { errOut := fmt.Errorf("%s interface name not found in the host", ifaceName) log.Error().Err(errOut) return errOut } c.Mu.Lock() defer c.Mu.Unlock() if !c.HostConfig.BpfChainingEnabled { errout := c.AddProgramWithoutChaining(ifaceName, bpfProgs) if errout != nil { return errout } return nil } for _, bpfProg := range bpfProgs.XDPIngress { if c.IngressXDPBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.IngressXDPBpfs[ifaceName] = list.New() if err := c.VerifyAndStartXDPRootProgram(ifaceName, models.XDPIngressType); err != nil { c.IngressXDPBpfs[ifaceName] = nil return fmt.Errorf("failed to chain XDP BPF programs: %w", err) } log.Info().Msgf("Push Back and Start XDP program : %s seq_id : %d", bpfProg.Name, bpfProg.SeqID) if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.XDPIngressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } } else if err := c.AddAndStartBPF(bpfProg, ifaceName, models.XDPIngressType); err != nil { return fmt.Errorf("failed to AddAndStartBPF xdp BPF Program: %w", err) } } for _, bpfProg := range bpfProgs.TCIngress { if c.IngressTCBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.IngressTCBpfs[ifaceName] = list.New() if err := c.VerifyAndStartTCRootProgram(ifaceName, models.IngressType); err != nil { c.IngressTCBpfs[ifaceName] = nil return fmt.Errorf("failed to chain ingress tc bpf programs: %w", err) } if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.IngressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } } else if err := c.AddAndStartBPF(bpfProg, ifaceName, models.IngressType); err != nil { return fmt.Errorf("failed to AddAndStartBPF tcingress BPF Program: %w", err) } } for _, bpfProg := range bpfProgs.TCEgress { if c.EgressTCBpfs[ifaceName] == nil { if bpfProg.AdminStatus == models.Enabled { c.EgressTCBpfs[ifaceName] = list.New() if err := c.VerifyAndStartTCRootProgram(ifaceName, models.EgressType); err != nil { c.EgressTCBpfs[ifaceName] = nil return fmt.Errorf("failed to chain ingress tc bpf programs: %w", err) } if err := c.PushBackAndStartBPF(bpfProg, ifaceName, models.EgressType); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } } else if err := c.AddAndStartBPF(bpfProg, ifaceName, models.EgressType); err != nil { return fmt.Errorf("failed to AddAndStartBPF tcegress BPF Program: %w", err) } } return nil } // AddeBPFPrograms - Starts eBPF programs on the node if they are not running func (c *NFConfigs) AddeBPFPrograms(bpfProgs []models.L3afBPFPrograms) error { var combinedError error for _, bpfProg := range bpfProgs { c.Ifaces = map[string]string{bpfProg.Iface: bpfProg.IPv4Address} if len(c.Ifaces) == 0 { c.Ifaces = map[string]string{bpfProg.Iface: bpfProg.IPv4Address} } else { c.Ifaces[bpfProg.Iface] = bpfProg.IPv4Address } if err := c.AddProgramsOnInterface(bpfProg.Iface, bpfProg.HostName, bpfProg.BpfPrograms); err != nil { if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("add eBPF Programs failed to save configs %w", err)) } combinedError = errors.Join(combinedError, fmt.Errorf("failed to Add BPF program on iface %s with error: %w", bpfProg.Iface, err)) } if err := c.AddProbePrograms(bpfProg.HostName, bpfProg.BpfPrograms.Probes); err != nil { if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("add eBPF Programs of type probes failed to save configs %w", err)) } combinedError = errors.Join(combinedError, fmt.Errorf("failed to Add eBPF program of type probe with error: %w", err)) } } if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("AddeBPFPrograms failed to save configs %w", err)) } return combinedError } // CleanupProgramsOnInterface removes all EBPF program and its metadata, on the network interface provided func (c *NFConfigs) CleanupProgramsOnInterface(ifaceName string) { if c.IngressXDPBpfs[ifaceName] != nil { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.XDPIngressType); err != nil { log.Warn().Err(err).Msg("failed to Close Ingress XDP BPF Program") } } if c.IngressTCBpfs[ifaceName] != nil { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.IngressType); err != nil { log.Warn().Err(err).Msg("failed to Close Ingress XDP BPF Program") } } if c.EgressTCBpfs[ifaceName] != nil { if err := c.StopNRemoveAllBPFPrograms(ifaceName, models.EgressType); err != nil { log.Warn().Err(err).Msg("failed to Close Ingress XDP BPF Program") } } } // DeleteProgramsOnInterface : It will delete ebpf Programs on the given interface func (c *NFConfigs) DeleteProgramsOnInterface(ifaceName, HostName string, bpfProgs *models.BPFProgramNames) error { var err error if c.HostInterfaces, err = getHostInterfaces(); err != nil { errOut := fmt.Errorf("failed get interfaces in DeleteProgramsOnInterface Function: %v", err) log.Error().Err(errOut) return errOut } if HostName != c.HostName { errOut := fmt.Errorf("provided bpf programs do not belong to this host") log.Error().Err(errOut) return errOut } if ifaceName == "" || bpfProgs == nil { errOut := fmt.Errorf("iface name or bpf programs are empty") log.Error().Err(errOut) return errOut } if _, ok := c.HostInterfaces[ifaceName]; !ok { c.CleanupProgramsOnInterface(ifaceName) errOut := fmt.Errorf("%s interface name not found in the host, Stop called, %w", ifaceName, err) log.Error().Err(errOut) return errOut } c.Mu.Lock() defer c.Mu.Unlock() sort.Strings(bpfProgs.XDPIngress) if c.IngressXDPBpfs[ifaceName] != nil { bpfList := c.IngressXDPBpfs[ifaceName] for e := bpfList.Front(); e != nil; { next := e.Next() data := e.Value.(*BPF) if BinarySearch(bpfProgs.XDPIngress, data.Program.Name) { err := c.DeleteProgramsOnInterfaceHelper(e, ifaceName, models.XDPIngressType, bpfList) if err != nil { return fmt.Errorf("DeleteProgramsOnInterfaceHelper function failed : %w", err) } } e = next } if bpfList.Len() == 0 { c.IngressXDPBpfs[ifaceName] = nil } } sort.Strings(bpfProgs.TCIngress) if c.IngressTCBpfs[ifaceName] != nil { bpfList := c.IngressTCBpfs[ifaceName] for e := bpfList.Front(); e != nil; { next := e.Next() data := e.Value.(*BPF) if BinarySearch(bpfProgs.TCIngress, data.Program.Name) { err := c.DeleteProgramsOnInterfaceHelper(e, ifaceName, models.IngressType, bpfList) if err != nil { return fmt.Errorf("DeleteProgramsOnInterfaceHelper function failed : %w", err) } } e = next } if bpfList.Len() == 0 { c.IngressTCBpfs[ifaceName] = nil } } sort.Strings(bpfProgs.TCEgress) if c.EgressTCBpfs[ifaceName] != nil { bpfList := c.EgressTCBpfs[ifaceName] for e := bpfList.Front(); e != nil; { next := e.Next() data := e.Value.(*BPF) if BinarySearch(bpfProgs.TCEgress, data.Program.Name) { err := c.DeleteProgramsOnInterfaceHelper(e, ifaceName, models.EgressType, bpfList) if err != nil { return fmt.Errorf("DeleteProgramsOnInterfaceHelper function failed : %w", err) } } e = next } if bpfList.Len() == 0 { c.EgressTCBpfs[ifaceName] = nil } } sort.Strings(bpfProgs.Probes) for e := c.ProbesBpfs.Front(); e != nil; { next := e.Next() data := e.Value.(*BPF) if BinarySearch(bpfProgs.Probes, data.Program.Name) { err := c.DeleteProgramsOnInterfaceHelper(e, ifaceName, "", &c.ProbesBpfs) if err != nil { return fmt.Errorf("DeleteProgramsOnInterfaceHelper function failed : %w", err) } } e = next } return nil } // DeleteProgramsOnInterfaceHelper : helper function for DeleteProgramsOnInterface function func (c *NFConfigs) DeleteProgramsOnInterfaceHelper(e *list.Element, ifaceName string, direction string, bpfList *list.List) error { if e == nil { return nil } prog := e.Value.(*BPF) prog.Program.AdminStatus = models.Disabled if err := prog.Stop(ifaceName, c.Ifaces[ifaceName], direction, c.HostConfig.BpfChainingEnabled); err != nil { return fmt.Errorf("failed to stop %s iface %s direction %s with err %w", prog.Program.Name, ifaceName, direction, err) } tmpNextBPF := e.Next() tmpPreviousBPF := e.Prev() bpfList.Remove(e) if !c.HostConfig.BpfChainingEnabled { return nil } if tmpNextBPF != nil && tmpNextBPF.Prev() != nil { // relink the next element if err := c.LinkBPFPrograms(tmpNextBPF.Prev().Value.(*BPF), tmpNextBPF.Value.(*BPF)); err != nil { log.Error().Err(err).Msgf("DeleteProgramsOnInterfaceHelper - failed LinkBPFPrograms") return fmt.Errorf("DeleteProgramsOnInterfaceHelper - failed LinkBPFPrograms %w", err) } } // check if the program is not probes if len(direction) > 1 { // Check if list contains root program only then stop the root program. if tmpPreviousBPF.Prev() == nil && tmpPreviousBPF.Next() == nil { log.Info().Msgf("no ebpf programs are running, stopping root program") if err := c.StopRootProgram(ifaceName, direction); err != nil { return fmt.Errorf("failed to stop to root program of iface %s direction %v with err %w", ifaceName, direction, err) } } } return nil } // DeleteEbpfPrograms - Delete eBPF programs on the node if they are running func (c *NFConfigs) DeleteEbpfPrograms(bpfProgs []models.L3afBPFProgramNames) error { var combinedError error for _, bpfProg := range bpfProgs { if err := c.DeleteProgramsOnInterface(bpfProg.Iface, bpfProg.HostName, bpfProg.BpfProgramNames); err != nil { if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("SaveConfigsToConfigStore failed to save configs %w", err)) } combinedError = errors.Join(combinedError, fmt.Errorf("failed to Remove eBPF program on iface %s with error: %w", bpfProg.Iface, err)) } } if err := c.SaveConfigsToConfigStore(); err != nil { combinedError = errors.Join(combinedError, fmt.Errorf("DeleteEbpfPrograms failed to save configs %w", err)) } return combinedError } // BinarySearch: It is checking a target string exists in sorted slice of strings func BinarySearch(names []string, target string) bool { left := 0 right := len(names) - 1 for right >= left { mid := (left + right) >> 1 if names[mid] == target { return true } else if names[mid] > target { right = mid - 1 } else { left = mid + 1 } } return false } func (c *NFConfigs) AddProbePrograms(HostName string, bpfProgs []*models.BPFProgram) error { if HostName != c.HostName { errOut := fmt.Errorf("provided bpf programs do not belong to this host") log.Error().Err(errOut) return errOut } if len(bpfProgs) == 0 { return nil } for _, bpfProg := range bpfProgs { if err := c.PushBackAndStartProbe(bpfProg); err != nil { return fmt.Errorf("failed to PushBackAndStartBPF BPF Program: %w", err) } } return nil } // PushBackAndStartProbe method inserts the element at the end of the list func (c *NFConfigs) PushBackAndStartProbe(bpfProg *models.BPFProgram) error { log.Info().Msgf("PushBackAndStartProbe: %s", bpfProg.Name) bpf := NewBpfProgram(c.Ctx, *bpfProg, c.HostConfig, "") if err := c.DownloadAndStartProbes(c.ProbesBpfs.PushBack(bpf)); err != nil { return fmt.Errorf("failed to download and start the BPF %s with err: %w", bpfProg.Name, err) } return nil } func (c *NFConfigs) DownloadAndStartProbes(element *list.Element) error { if element == nil { return fmt.Errorf("element is nil pointer") } bpf := element.Value.(*BPF) if err := bpf.VerifyAndGetArtifacts(c.HostConfig); err != nil { return fmt.Errorf("failed to get artifacts %s with error: %w", bpf.Program.Artifact, err) } if err := bpf.LoadBPFProgram(""); err != nil { return fmt.Errorf("failed to load bpf program %s with error: %w", bpf.Program.Name, err) } return nil } // SerialzeProgram this function wil serialize the program func SerialzeProgram(e *list.Element) *models.L3AFMetaData { tmp := &models.L3AFMetaData{} bpf := e.Value.(*BPF) tmp.BpfMaps = make([]string, 0) if bpf.BpfMaps != nil { for _, v := range bpf.BpfMaps { tmp.BpfMaps = append(tmp.BpfMaps, v.Name) } } tmp.FilePath = bpf.FilePath tmp.MapNamePath = bpf.MapNamePath tmp.PrevMapNamePath = bpf.PrevMapNamePath tmp.PrevProgMapID = uint32(bpf.PrevProgMapID) tmp.ProgID = uint32(bpf.ProgID) tmp.Program = bpf.Program tmp.ProgMapID = uint32(bpf.ProgMapID) tmp.RestartCount = bpf.RestartCount tmp.ProgMapCollection = models.MetaColl{ Programs: make([]string, 0), Maps: make([]string, 0), } if bpf.ProgMapCollection != nil { for k, v := range bpf.ProgMapCollection.Programs { if v.Type() == ebpf.XDP || v.Type() == ebpf.SchedACT || v.Type() == ebpf.SchedCLS { tmp.ProgMapCollection.Programs = append(tmp.ProgMapCollection.Programs, k) } } for k := range bpf.ProgMapCollection.Maps { tmp.ProgMapCollection.Maps = append(tmp.ProgMapCollection.Maps, k) } } tmp.MetricsBpfMaps = make(map[string]models.MetaMetricsBPFMap) if bpf.MetricsBpfMaps != nil { for k1, v1 := range bpf.MetricsBpfMaps { values := make([]float64, 0) tmpval := v1.Values for i := 0; i < v1.Values.Len(); i++ { if tmpval.Value != nil { values = append(values, tmpval.Value.(float64)) } tmpval = tmpval.Next() } tmp.MetricsBpfMaps[k1] = models.MetaMetricsBPFMap{ MapName: v1.Name, Key: v1.Key, Values: values, Aggregator: v1.Aggregator, LastValue: float64(v1.LastValue), } } } tmp.Link = false if bpf.Link != nil { tmp.Link = true } return tmp } // GetL3AFHOSTDATA this function will give serialize form of current l3afd state func (c *NFConfigs) GetL3AFHOSTDATA() models.L3AFALLHOSTDATA { result := models.L3AFALLHOSTDATA{} result.HostName = c.HostName result.Ifaces = c.Ifaces result.HostInterfaces = c.HostInterfaces result.IngressXDPBpfs = make(map[string][]*models.L3AFMetaData) result.IngressTCBpfs = make(map[string][]*models.L3AFMetaData) result.EgressTCBpfs = make(map[string][]*models.L3AFMetaData) result.ProbesBpfs = make([]models.L3AFMetaData, 0) if c.IngressXDPBpfs != nil { for k, v := range c.IngressXDPBpfs { ls := make([]*models.L3AFMetaData, 0) for e := v.Front(); e != nil; e = e.Next() { ls = append(ls, SerialzeProgram(e)) } result.IngressXDPBpfs[k] = ls } } if c.IngressTCBpfs != nil { for k, v := range c.IngressTCBpfs { ls := make([]*models.L3AFMetaData, 0) for e := v.Front(); e != nil; e = e.Next() { ls = append(ls, SerialzeProgram(e)) } result.IngressTCBpfs[k] = ls } } if c.EgressTCBpfs != nil { for k, v := range c.EgressTCBpfs { ls := make([]*models.L3AFMetaData, 0) for e := v.Front(); e != nil; e = e.Next() { ls = append(ls, SerialzeProgram(e)) } result.EgressTCBpfs[k] = ls } } for e := c.ProbesBpfs.Front(); e != nil; e = e.Next() { result.ProbesBpfs = append(result.ProbesBpfs, *SerialzeProgram(e)) } metrics, _ := prometheus.DefaultGatherer.Gather() result.AllStats = make([]models.MetricVec, 0) listofMetrics := []string{"l3afd_BPFStartCount", "l3afd_BPFStopCount", "l3afd_BPFUpdateCount", "l3afd_BPFUpdateFailedCount", "l3afd_BPFRunning", "l3afd_BPFStartTime", "l3afd_BPFMonitorMap"} for _, metric := range metrics { name := *metric.Name tp := metric.Type.Number() if slices.Index(listofMetrics, name) != -1 { for _, m := range metric.Metric { r := models.MetricVec{} lt := m.GetLabel() for _, y := range lt { r.Labels = append(r.Labels, models.Label{ Name: *y.Name, Value: *y.Value, }) } r.MetricName = name r.Type = int32(tp) if r.Type == 0 { r.Value = m.GetCounter().GetValue() } else { r.Value = m.Gauge.GetValue() } result.AllStats = append(result.AllStats, r) } } } return result } // StartAllUserProgramsAndProbes this function will restart all the User Programs and probes func (c *NFConfigs) StartAllUserProgramsAndProbes() error { if c.IngressXDPBpfs != nil { for iface, v := range c.IngressXDPBpfs { for e := v.Front(); e != nil; e = e.Next() { // Starting Probes b := e.Value.(*BPF) ef := b.Program.EntryFunctionName b.Program.EntryFunctionName = "" prg := b.ProgMapCollection b.ProgMapCollection = nil if err := b.LoadBPFProgram(iface); err != nil { return fmt.Errorf("unable to load probes %w", err) } b.Program.EntryFunctionName = ef if b.ProgMapCollection != nil { for fk, vf := range b.ProgMapCollection.Programs { if _, ok := prg.Programs[fk]; !ok { prg.Programs[fk] = vf } else { vf.Close() } } for fk, vf := range b.ProgMapCollection.Maps { if _, ok := prg.Maps[fk]; !ok { prg.Maps[fk] = vf } else { vf.Close() } } } b.ProgMapCollection = prg if len(b.Program.CmdStart) > 0 { // Verify other instance is running if err := StopExternalRunningProcess(b.Program.CmdStart); err != nil { return fmt.Errorf("failed to stop external instance of the program %s with error : %w", b.Program.CmdStart, err) } } if b.Program.UserProgramDaemon { // Starting User Program if err := b.StartUserProgram(iface, models.XDPIngressType, c.HostConfig.BpfChainingEnabled); err != nil { return err } } } } } if c.IngressTCBpfs != nil { for iface, v := range c.IngressTCBpfs { for e := v.Front(); e != nil; e = e.Next() { b := e.Value.(*BPF) ef := b.Program.EntryFunctionName b.Program.EntryFunctionName = "" prg := b.ProgMapCollection b.ProgMapCollection = nil if err := b.LoadBPFProgram(iface); err != nil { return fmt.Errorf("unable to load probes %w", err) } b.Program.EntryFunctionName = ef if b.ProgMapCollection != nil { for fk, vf := range b.ProgMapCollection.Programs { if _, ok := prg.Programs[fk]; !ok { prg.Programs[fk] = vf } else { vf.Close() } } for fk, vf := range b.ProgMapCollection.Maps { if _, ok := prg.Maps[fk]; !ok { prg.Maps[fk] = vf } else { vf.Close() } } } b.ProgMapCollection = prg if len(b.Program.CmdStart) > 0 { // Verify other instance is running if err := StopExternalRunningProcess(b.Program.CmdStart); err != nil { return fmt.Errorf("failed to stop external instance of the program %s with error : %w", b.Program.CmdStart, err) } } if b.Program.UserProgramDaemon { // Starting User Program if err := b.StartUserProgram(iface, models.XDPIngressType, c.HostConfig.BpfChainingEnabled); err != nil { return err } } } } } if c.EgressTCBpfs != nil { for iface, v := range c.EgressTCBpfs { for e := v.Front(); e != nil; e = e.Next() { b := e.Value.(*BPF) ef := b.Program.EntryFunctionName b.Program.EntryFunctionName = "" prg := b.ProgMapCollection b.ProgMapCollection = nil if err := b.LoadBPFProgram(iface); err != nil { return fmt.Errorf("unable to load probes %w", err) } b.Program.EntryFunctionName = ef if b.ProgMapCollection != nil { for fk, vf := range b.ProgMapCollection.Programs { if _, ok := prg.Programs[fk]; !ok { prg.Programs[fk] = vf } else { vf.Close() } } for fk, vf := range b.ProgMapCollection.Maps { if _, ok := prg.Maps[fk]; !ok { prg.Maps[fk] = vf } else { vf.Close() } } } b.ProgMapCollection = prg if len(b.Program.CmdStart) > 0 { // Verify other instance is running if err := StopExternalRunningProcess(b.Program.CmdStart); err != nil { return fmt.Errorf("failed to stop external instance of the program %s with error : %w", b.Program.CmdStart, err) } } if b.Program.UserProgramDaemon { // Starting User Program if err := b.StartUserProgram(iface, models.XDPIngressType, c.HostConfig.BpfChainingEnabled); err != nil { return err } } } } } return nil } // StopAllProbesAndUserPrograms this function will stop all the probes & stops user programs func (c *NFConfigs) StopAllProbesAndUserPrograms() error { if c.IngressXDPBpfs != nil { for iface, v := range c.IngressXDPBpfs { for e := v.Front(); e != nil; e = e.Next() { // Stopping Probes b := e.Value.(*BPF) if b.ProbeLinks != nil { for _, pb := range b.ProbeLinks { (*pb).Close() } } b.ProbeLinks = make([]*link.Link, 0) // Stopping UserPrograms if err := b.StopUserProgram(iface, models.XDPIngressType); err != nil { return fmt.Errorf("failed to stop user program : %w", err) } } } } if c.IngressTCBpfs != nil { for iface, v := range c.IngressTCBpfs { for e := v.Front(); e != nil; e = e.Next() { // Stopping Probes b := e.Value.(*BPF) if b.ProbeLinks != nil { for _, pb := range b.ProbeLinks { (*pb).Close() } } b.ProbeLinks = make([]*link.Link, 0) // Stopping UserPrograms if err := b.StopUserProgram(iface, models.XDPIngressType); err != nil { return fmt.Errorf("failed to stop user program : %w", err) } } } } if c.EgressTCBpfs != nil { for iface, v := range c.EgressTCBpfs { for e := v.Front(); e != nil; e = e.Next() { // Stopping Probes b := e.Value.(*BPF) if b.ProbeLinks != nil { for _, pb := range b.ProbeLinks { (*pb).Close() } } b.ProbeLinks = make([]*link.Link, 0) // Stopping UserPrograms if err := b.StopUserProgram(iface, models.XDPIngressType); err != nil { return fmt.Errorf("failed to stop user program : %w", err) } } } } return nil } ================================================ FILE: bpfprogs/nfconfig_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "container/list" "context" "os" "path/filepath" "reflect" "sync" "testing" "time" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/rs/zerolog/log" ) var ( machineHostname string HostInterfaces map[string]bool pMon *PCheck mMon *BpfMetrics valVerChange *models.BPFPrograms valStatusChange *models.BPFPrograms ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List ifaceName string seqID int bpfProgs *models.BPFPrograms ) func setupDBTest() { machineHostname, _ = os.Hostname() HostInterfaces = make(map[string]bool) HostInterfaces["fakeif0"] = true pMon = NewPCheck(3, true, 10) mMon = NewpBpfMetrics(true, 30) ingressXDPBpfs = make(map[string]*list.List) ingressTCBpfs = make(map[string]*list.List) egressTCBpfs = make(map[string]*list.List) } func setupValidBPF() { bpf := BPF{ Program: models.BPFProgram{ ID: 1, Name: "foo", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", Version: "1.0", UserProgramDaemon: true, AdminStatus: "DISABLED", }, Cmd: nil, FilePath: "", RestartCount: 0, } ifaceName = "dummy" seqID = 1 log.Info().Msg(bpf.Program.Name) } func setupBPFProgramData() { bpfProgsTmp := &models.BPFPrograms{} ifaceName = "dummy" seqID = 1 bpfProg := &models.BPFProgram{ ID: 1, Name: "foo", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", Version: "1.0", UserProgramDaemon: true, AdminStatus: "ENABLED", SeqID: 1, } bpfProgsTmp.XDPIngress = append(bpfProgsTmp.XDPIngress, bpfProg) bpfProgs = bpfProgsTmp } func setupBPFProgramVersionChange() { bpfProgsTmp := &models.BPFPrograms{} ifaceName = "dummy" seqID = 1 bpfProg := &models.BPFProgram{ ID: 1, Name: "foo", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", Version: "2.0", UserProgramDaemon: true, AdminStatus: "ENABLED", } bpfProgsTmp.XDPIngress = append(bpfProgsTmp.XDPIngress, bpfProg) valVerChange = bpfProgsTmp } func setupBPFProgramStatusChange() { bpfProgsTmp := &models.BPFPrograms{} //cfg := make(map[string][]*models.BPFProgram) ifaceName = "dummy" seqID = 1 bpfProg := &models.BPFProgram{ ID: 1, Name: "foo", Artifact: "foo.tar.gz", CmdStart: "foo", CmdStop: "", Version: "2.0", UserProgramDaemon: true, AdminStatus: "DISABLED", } bpfProgsTmp.XDPIngress = append(bpfProgsTmp.XDPIngress, bpfProg) valStatusChange = bpfProgsTmp } func TestNewNFConfigs(t *testing.T) { type args struct { host string hostConf *config.Config pMon *PCheck mMon *BpfMetrics ctx context.Context } setupDBTest() hostIfaces, _ := getHostInterfaces() tests := []struct { name string args args want *NFConfigs wantErr bool }{ {name: "EmptyConfig", args: args{ host: machineHostname, hostConf: nil, pMon: pMon, mMon: mMon}, want: &NFConfigs{HostName: machineHostname, HostInterfaces: hostIfaces, IngressXDPBpfs: ingressXDPBpfs, IngressTCBpfs: ingressTCBpfs, EgressTCBpfs: egressTCBpfs, HostConfig: nil, ProcessMon: pMon, BpfMetricsMon: mMon, Mu: new(sync.Mutex), Ifaces: make(map[string]string), }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := NewNFConfigs(tt.args.ctx, tt.args.host, tt.args.hostConf, tt.args.pMon, tt.args.mMon) if (err != nil) != tt.wantErr { t.Errorf("NewNFConfigs() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("NewNFConfigs() = %#v, want %#v", got, tt.want) } }) } } func TestNFConfigs_Deploy(t *testing.T) { type fields struct { hostName string HostInterfaces map[string]bool ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck metricsMon *BpfMetrics } type args struct { iface string hostName string bpfProgs *models.BPFPrograms } setupDBTest() setupValidBPF() setupBPFProgramData() setupBPFProgramVersionChange() setupBPFProgramStatusChange() HostInterfaces, err := getHostInterfaces() if err != nil { log.Info().Msg("getHostInterfaces returned and error") } var HostInterfacesKey string var HostInterfacesValue bool for HostInterfacesKey, HostInterfacesValue = range HostInterfaces { log.Debug().Msgf("HostInterfacesKey: %v, HostInterfacesValue: %v", HostInterfacesKey, HostInterfacesValue) break } tests := []struct { name string fields fields args args wantErr bool }{ { name: "EmptyBPFs", fields: fields{ hostName: machineHostname, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: nil, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: "", hostName: machineHostname, bpfProgs: nil, }, wantErr: true, }, { name: "InvalidHostName", fields: fields{ hostName: machineHostname, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: nil, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: "dummy", hostName: "dummy", bpfProgs: bpfProgs, }, wantErr: true, }, { name: "ValidHostNameInvalidIfaceName", fields: fields{ hostName: machineHostname, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: nil, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: "dummy", hostName: machineHostname, bpfProgs: &models.BPFPrograms{}, }, wantErr: true, }, { name: "ValidHostNameValidIfaceName", fields: fields{ hostName: machineHostname, HostInterfaces: HostInterfaces, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: nil, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: HostInterfacesKey, hostName: machineHostname, bpfProgs: &models.BPFPrograms{}, }, wantErr: false, }, { name: "TestEBPFRepoDownload", fields: fields{ hostName: machineHostname, HostInterfaces: HostInterfaces, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: &config.Config{BPFDir: "/tmp", EBPFRepoURL: "http://www.example.com"}, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: HostInterfacesKey, hostName: machineHostname, bpfProgs: bpfProgs, }, wantErr: false, }, { name: "NewBPFWithVersionChange", fields: fields{ hostName: machineHostname, HostInterfaces: HostInterfaces, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: &config.Config{BPFDir: "/tmp", EBPFRepoURL: "http://www.example.com"}, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: HostInterfacesKey, hostName: machineHostname, bpfProgs: valVerChange, }, wantErr: false, }, { name: "NewBPFWithStatusChange", fields: fields{ hostName: machineHostname, HostInterfaces: HostInterfaces, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: &config.Config{BPFDir: "/tmp", EBPFRepoURL: "http://www.example.com"}, ProcessMon: pMon, metricsMon: mMon, }, args: args{ iface: HostInterfacesKey, hostName: machineHostname, bpfProgs: valStatusChange, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.fields.hostName, // configs: tt.fields.configs, HostInterfaces: tt.fields.HostInterfaces, IngressXDPBpfs: tt.fields.ingressXDPBpfs, IngressTCBpfs: tt.fields.ingressTCBpfs, EgressTCBpfs: tt.fields.egressTCBpfs, HostConfig: tt.fields.hostConfig, ProcessMon: tt.fields.ProcessMon, Mu: new(sync.Mutex), } if err := cfg.Deploy(tt.args.iface, tt.args.hostName, tt.args.bpfProgs); (err != nil) != tt.wantErr { t.Errorf("NFConfigs.Deploy() error = %#v, wantErr %#v", err, tt.wantErr) } }) } } func TestNFConfigs_Close(t *testing.T) { type fields struct { hostName string ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck } tests := []struct { name string fields fields wantErr bool }{ { name: "EmptyMap", fields: fields{ hostName: machineHostname, ingressXDPBpfs: make(map[string]*list.List), ingressTCBpfs: make(map[string]*list.List), egressTCBpfs: make(map[string]*list.List), hostConfig: &config.Config{ BpfMapDefaultPath: "/sys/fs/bpf", ShutdownTimeout: 30 * time.Second, }, ProcessMon: pMon, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.fields.hostName, IngressXDPBpfs: tt.fields.ingressXDPBpfs, IngressTCBpfs: tt.fields.ingressTCBpfs, EgressTCBpfs: tt.fields.egressTCBpfs, HostConfig: tt.fields.hostConfig, ProcessMon: tt.fields.ProcessMon, } ctx, cancelfunc := context.WithTimeout(context.Background(), 30*time.Second) defer cancelfunc() if err := cfg.Close(ctx); (err != nil) != tt.wantErr { t.Errorf("NFConfigs.Close() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_getHostInterfaces(t *testing.T) { tests := []struct { name string wantErr bool }{ { name: "GoodInput", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := getHostInterfaces() if (err != nil) != tt.wantErr { t.Errorf("getHostInterfaces() error : %v", err) } }) } } func Test_BinarySearch(t *testing.T) { tests := []struct { name string vals []string target string result bool }{ { name: "FoundTheTarget", vals: []string{"connection-limit", "ipfix-flow-exporter", "ratelimiting"}, target: "ratelimiting", result: true, }, { name: "DidNotFindTheTarget", vals: []string{"connection-limit", "ipfix-flow-exporter", "ratelimiting"}, target: "zsdf", result: false, }, } for _, tt := range tests { if BinarySearch(tt.vals, tt.target) != tt.result { t.Errorf("BinarySearch is not producing expected output") } } } func Test_AddProgramsOnInterface(t *testing.T) { HostInterfaces, err := getHostInterfaces() if err != nil { log.Info().Msg("getHostInterfaces returned and error") } var HostInterfacesKey string var HostInterfacesValue bool for HostInterfacesKey, HostInterfacesValue = range HostInterfaces { log.Debug().Msgf("HostInterfacesKey: %v, HostInterfacesValue: %v", HostInterfacesKey, HostInterfacesValue) break } type fields struct { hostName string HostInterfaces map[string]bool ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck mu *sync.Mutex } type args struct { iface string hostName string bpfProgs *models.BPFPrograms } tests := []struct { name string field fields arg args wanterr bool }{ { name: "UnknownHostName", field: fields{}, arg: args{}, wanterr: true, }, { name: "NilInterface", field: fields{ hostName: "l3af-local-test", }, arg: args{ hostName: "fakeif0", }, wanterr: true, }, { name: "UnknownInterface", field: fields{ hostName: "l3af-local-test", }, arg: args{ hostName: "l3af-local-test", iface: "dummyinterface", bpfProgs: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{ &models.BPFProgram{ Name: "dummy_name", SeqID: 1, Artifact: "dummy_artifact.tar.gz", MapName: "xdp_rl_ingress_next_prog", CmdStart: "dummy_command", Version: "latest", UserProgramDaemon: true, AdminStatus: "enabled", ProgType: "xdp", CfgVersion: 1, }, }, }, }, wanterr: true, }, { name: "GoodInput", field: fields{ hostName: "l3af-local-test", HostInterfaces: HostInterfaces, mu: new(sync.Mutex), ingressXDPBpfs: map[string]*list.List{"fakeif0": nil}, ingressTCBpfs: map[string]*list.List{"fakeif0": nil}, egressTCBpfs: map[string]*list.List{"fakeif0": nil}, hostConfig: &config.Config{ BpfChainingEnabled: true, }, }, arg: args{ hostName: "l3af-local-test", iface: HostInterfacesKey, bpfProgs: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{}, TCEgress: []*models.BPFProgram{}, TCIngress: []*models.BPFProgram{}, }, }, wanterr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.field.hostName, IngressXDPBpfs: tt.field.ingressXDPBpfs, IngressTCBpfs: tt.field.ingressTCBpfs, EgressTCBpfs: tt.field.egressTCBpfs, HostConfig: tt.field.hostConfig, ProcessMon: tt.field.ProcessMon, HostInterfaces: tt.field.HostInterfaces, Mu: tt.field.mu, } err := cfg.AddProgramsOnInterface(tt.arg.iface, tt.arg.hostName, tt.arg.bpfProgs) if (err != nil) != tt.wanterr { t.Errorf("AddProgramsOnInterface: %v", err) } }) } } func TestAddeBPFPrograms(t *testing.T) { HostInterfaces, err := getHostInterfaces() if err != nil { log.Info().Msg("getHostInterfaces returned and error") } var HostInterfacesKey string var HostInterfacesValue bool for HostInterfacesKey, HostInterfacesValue = range HostInterfaces { log.Debug().Msgf("HostInterfacesKey: %v, HostInterfacesValue: %v", HostInterfacesKey, HostInterfacesValue) break } type fields struct { hostName string HostInterfaces map[string]bool ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck mu *sync.Mutex ifaces map[string]string } tests := []struct { name string field fields arg []models.L3afBPFPrograms wanterr bool }{ { name: "UnknownHostName", field: fields{ hostName: "l3af-prod", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFPrograms{ { HostName: "l3af-test", Iface: "fakeif0", BpfPrograms: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{}, TCIngress: []*models.BPFProgram{}, TCEgress: []*models.BPFProgram{}, }, }, }, wanterr: true, }, { name: "NilInterface", field: fields{ hostName: "l3af-local-test", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFPrograms{ { HostName: "l3af-local-test", BpfPrograms: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{}, TCIngress: []*models.BPFProgram{}, TCEgress: []*models.BPFProgram{}, }, }, }, wanterr: true, }, { name: "UnknownInterface", field: fields{ hostName: "l3af-local-test", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFPrograms{ { HostName: "l3af-local-test", Iface: "dummyinterface", BpfPrograms: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{ &models.BPFProgram{ Name: "dummy_name", SeqID: 1, Artifact: "dummy_artifact_name", MapName: "xdp_rl_ingress_next_prog", CmdStart: "dummy_command", Version: "latest", UserProgramDaemon: true, AdminStatus: "enabled", ProgType: "xdp", CfgVersion: 1, }, }, }, }, }, wanterr: true, }, { name: "GoodInput", field: fields{ hostName: "l3af-local-test", HostInterfaces: HostInterfaces, // fakeif0 is a fake interface mu: new(sync.Mutex), ingressXDPBpfs: map[string]*list.List{"fakeif0": nil}, ingressTCBpfs: map[string]*list.List{"fakeif0": nil}, egressTCBpfs: map[string]*list.List{"fakeif0": nil}, ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFPrograms{ { HostName: "l3af-local-test", Iface: HostInterfacesKey, BpfPrograms: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{}, TCIngress: []*models.BPFProgram{}, TCEgress: []*models.BPFProgram{}, }, }, }, wanterr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.field.hostName, IngressXDPBpfs: tt.field.ingressXDPBpfs, IngressTCBpfs: tt.field.ingressTCBpfs, EgressTCBpfs: tt.field.egressTCBpfs, HostConfig: tt.field.hostConfig, ProcessMon: tt.field.ProcessMon, HostInterfaces: tt.field.HostInterfaces, Mu: tt.field.mu, } err := cfg.AddeBPFPrograms(tt.arg) if (err != nil) != tt.wanterr { t.Errorf("AddeBPFPrograms failed: %v", err) } }) } } func TestDeleteProgramsOnInterface(t *testing.T) { type fields struct { hostName string HostInterfaces map[string]bool ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck mu *sync.Mutex } type args struct { iface string hostName string bpfProgs *models.BPFProgramNames } tests := []struct { name string field fields arg args wanterr bool }{ { name: "UnknownHostName", field: fields{}, arg: args{}, wanterr: true, }, { name: "NilInterface", field: fields{ hostName: "l3af-local-test", }, arg: args{ hostName: "fakeif0", }, wanterr: true, }, { name: "UnknownInterface", field: fields{ hostName: "l3af-local-test", }, arg: args{ hostName: "l3af-local-test", iface: "dummyinterface", bpfProgs: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, wanterr: true, }, { name: "GoodInputButNotRealInterface", field: fields{ hostName: "l3af-local-test", HostInterfaces: map[string]bool{"fakeif0": true}, mu: new(sync.Mutex), ingressXDPBpfs: map[string]*list.List{"fakeif0": nil}, ingressTCBpfs: map[string]*list.List{"fakeif0": nil}, egressTCBpfs: map[string]*list.List{"fakeif0": nil}, }, arg: args{ hostName: "l3af-local-test", iface: "fakeif0", bpfProgs: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, wanterr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.field.hostName, IngressXDPBpfs: tt.field.ingressXDPBpfs, IngressTCBpfs: tt.field.ingressTCBpfs, EgressTCBpfs: tt.field.egressTCBpfs, HostConfig: tt.field.hostConfig, ProcessMon: tt.field.ProcessMon, HostInterfaces: tt.field.HostInterfaces, Mu: tt.field.mu, } err := cfg.DeleteProgramsOnInterface(tt.arg.iface, tt.arg.hostName, tt.arg.bpfProgs) if (err != nil) != tt.wanterr { t.Errorf("DeleteProgramsOnInterface failed: %v", err) } }) } } func TestDeleteEbpfPrograms(t *testing.T) { type fields struct { hostName string HostInterfaces map[string]bool ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config ProcessMon *PCheck mu *sync.Mutex ifaces map[string]string } tests := []struct { name string field fields arg []models.L3afBPFProgramNames wanterr bool }{ { name: "UnknowhostName", field: fields{ hostName: "l3af-prod", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFProgramNames{ { HostName: "l3af-local-test", Iface: "fakeif0", BpfProgramNames: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, }, wanterr: true, }, { name: "NilInterface", field: fields{ hostName: "l3af-local-test", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFProgramNames{ { HostName: "l3af-local-test", Iface: "fakeif0", BpfProgramNames: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, }, wanterr: true, }, { name: "UnknownInterface", field: fields{ hostName: "l3af-local-test", ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFProgramNames{ { HostName: "l3af-local-test", Iface: "fakeif0", BpfProgramNames: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, }, wanterr: true, }, { name: "GoodInputButNotRealInterface", field: fields{ hostName: "l3af-local-test", HostInterfaces: map[string]bool{"fakeif0": true}, mu: new(sync.Mutex), ingressXDPBpfs: map[string]*list.List{"fakeif0": nil}, ingressTCBpfs: map[string]*list.List{"fakeif0": nil}, egressTCBpfs: map[string]*list.List{"fakeif0": nil}, ifaces: map[string]string{}, hostConfig: &config.Config{ L3afConfigStoreFileName: filepath.FromSlash("../testdata/Test_l3af-config.json"), }, }, arg: []models.L3afBPFProgramNames{ { HostName: "l3af-local-test", Iface: "fakeif0", BpfProgramNames: &models.BPFProgramNames{ XDPIngress: []string{}, TCIngress: []string{}, TCEgress: []string{}, }, }, }, wanterr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostName: tt.field.hostName, IngressXDPBpfs: tt.field.ingressXDPBpfs, IngressTCBpfs: tt.field.ingressTCBpfs, EgressTCBpfs: tt.field.egressTCBpfs, HostConfig: tt.field.hostConfig, ProcessMon: tt.field.ProcessMon, HostInterfaces: tt.field.HostInterfaces, Mu: tt.field.mu, } err := cfg.DeleteEbpfPrograms(tt.arg) if (err != nil) != tt.wanterr { t.Errorf("DeleteEbpfPrograms failed: %v", err) } }) } } func TestAddAndStartBPF(t *testing.T) { type field struct { ctx context.Context hostConfig *config.Config } type arg struct { bpfProg *models.BPFProgram direction string iface string } tests := []struct { name string fields field args arg wanterr bool }{ { name: "NilProgram", fields: field{}, args: arg{ bpfProg: nil, direction: "fakedirection", iface: "fakeif0", }, wanterr: true, }, { name: "AdminStatusDisabled", fields: field{}, args: arg{ bpfProg: &models.BPFProgram{ Name: "dummy", AdminStatus: "disabled", }, direction: "fakedirection", iface: "fakeif0", }, wanterr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ Ctx: tt.fields.ctx, HostConfig: tt.fields.hostConfig, } e := cfg.AddAndStartBPF(tt.args.bpfProg, tt.args.iface, tt.args.direction) if (e != nil) != tt.wanterr { t.Errorf("AddAndStartBPF failed : %v", e) } }) } } func TestAddProgramWithoutChaining(t *testing.T) { progList := list.New() progList.PushBack(&BPF{ Program: models.BPFProgram{ Name: "dummyProgram", }, }) type fields struct { ingressXDPBpfs map[string]*list.List ingressTCBpfs map[string]*list.List egressTCBpfs map[string]*list.List hostConfig *config.Config } type args struct { iface string hostName string bpfProgs *models.BPFPrograms } tests := []struct { name string field fields arg args wanterr bool }{ { name: "chainingEnabled", field: fields{ hostConfig: &config.Config{ BpfChainingEnabled: true, }, }, arg: args{ iface: "fakeif0", hostName: "fakehost", }, wanterr: false, }, { name: "badInput", field: fields{ hostConfig: &config.Config{ BpfChainingEnabled: false, }, ingressXDPBpfs: map[string]*list.List{"fakeif0": progList}, egressTCBpfs: map[string]*list.List{"fakeif0": progList}, ingressTCBpfs: map[string]*list.List{"fakeif0": progList}, }, arg: args{ iface: "fakeif0", bpfProgs: &models.BPFPrograms{ XDPIngress: []*models.BPFProgram{ { Name: "dummyProgram", AdminStatus: models.Enabled, }, }, TCIngress: []*models.BPFProgram{ { Name: "dummyProgram", AdminStatus: models.Enabled, }, }, TCEgress: []*models.BPFProgram{ { Name: "dummyProgram", AdminStatus: models.Enabled, }, }, }, }, wanterr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &NFConfigs{ HostConfig: tt.field.hostConfig, IngressXDPBpfs: tt.field.ingressXDPBpfs, EgressTCBpfs: tt.field.egressTCBpfs, IngressTCBpfs: tt.field.ingressTCBpfs, } e := cfg.AddProgramWithoutChaining(tt.arg.iface, tt.arg.bpfProgs) if (e != nil) != tt.wanterr { t.Errorf(" AddProgramWithoutChaining failed : %v", e) } }) } } ================================================ FILE: bpfprogs/probes.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !WINDOWS // +build !WINDOWS package bpfprogs import ( "fmt" "strings" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/rs/zerolog/log" ) func (b *BPF) LoadBPFProgramProbeType(prog *ebpf.Program, sectionName string) error { var progType, hookName, subType string switch prog.Type() { case ebpf.TracePoint: progType, hookName, subType = GetProgramSectionDetails(sectionName) tp, err := link.Tracepoint(hookName, subType, prog, nil) if err != nil { return fmt.Errorf("failed to link tracepoint sec name %s error %v", sectionName, err) } b.ProbeLinks = append(b.ProbeLinks, &tp) case ebpf.Kprobe: progType, hookName, _ = GetProgramSectionDetails(sectionName) kp, err := b.AttachProbePerfEvent(hookName, progType, prog) if err != nil { return fmt.Errorf("failed to attach perf event error %v", err) } b.ProbeLinks = append(b.ProbeLinks, &kp) default: return fmt.Errorf("un-supported probe type %s ", prog.Type()) } ebpfProgName := b.Program.Name + "_" + progType + "_" + hookName stats.Add(1, stats.BPFStartCount, ebpfProgName, "", "", "") return nil } // LoadBPFProgramProbeTypes - Load the BPF programs of probe types - TracePoint func (b *BPF) LoadBPFProgramProbeTypes(objSpec *ebpf.CollectionSpec) error { for i, prog := range b.ProgMapCollection.Programs { if prog.Type() == ebpf.XDP || prog.Type() == ebpf.SchedACT || prog.Type() == ebpf.SchedCLS { // skipping XDP/TC programs continue } if err := b.LoadBPFProgramProbeType(prog, objSpec.Programs[i].SectionName); err != nil { return err } for _, tmpMap := range b.Program.MonitorMaps { tmpMetricsMap := b.ProgMapCollection.Maps[tmpMap.Name] if tmpMetricsMap == nil { log.Error().Msgf("%s map is not loaded", tmpMap.Name) continue } } } return nil } // GetProgramSectionDetails returns group and name details // Section name format prog-type/hook/subtype // ret : prog-type, hook, subtype // e.g.: tracepoint/sock/inet_sock_set_state // e.g.: kprobe/sys_execve // e.g.: uprobe/:: func GetProgramSectionDetails(sectionName string) (string, string, string) { sections := strings.Split(sectionName, "/") switch strings.ToLower(sections[0]) { case models.TracePoint: return sections[0], sections[1], sections[2] case models.KProbe, models.KRetProbe: return sections[0], sections[1], "" case models.UProbe, models.URetProbe: var funcName string if len(sections) > 2 { funcName = strings.Join(sections[1:], "/") } return sections[0], funcName, "" default: return "", "", "" } } func (b *BPF) AttachProbePerfEvent(hookName, progType string, prog *ebpf.Program) (link.Link, error) { var kp link.Link var err error switch strings.ToLower(progType) { case models.KProbe: kp, err = link.Kprobe(hookName, prog, nil) if err != nil { return nil, fmt.Errorf("failed to link kprobe hook name %s error %v", hookName, err) } case models.KRetProbe: kp, err = link.Kretprobe(hookName, prog, nil) if err != nil { return nil, fmt.Errorf("failed to link kretprobe hook name %s error %v", hookName, err) } case models.UProbe: kp, err = b.AttachUProbePerfEvent(hookName, prog) if err != nil { return nil, fmt.Errorf("failed to attach uprobe program %v", err) } case models.URetProbe: kp, err = b.AttachURetProbePerfEvent(hookName, prog) if err != nil { return nil, fmt.Errorf("failed to attach uretprobe program %v", err) } default: return nil, fmt.Errorf("unsupported perf event progType: %s", progType) } return kp, nil } func (b *BPF) AttachUProbePerfEvent(hookName string, prog *ebpf.Program) (link.Link, error) { var kp link.Link funcNames := strings.Split(hookName, ":") ex, err := link.OpenExecutable(funcNames[0]) if err != nil { return nil, fmt.Errorf("uprobe failed to openExecutable binary file %s error %v", hookName, err) } kp, err = ex.Uprobe(getSymbolName(funcNames), prog, nil) if err != nil { return nil, fmt.Errorf("failed to link uprobe symbol %s - %v", getSymbolName(funcNames), err) } return kp, nil } func (b *BPF) AttachURetProbePerfEvent(hookName string, prog *ebpf.Program) (link.Link, error) { var kp link.Link funcNames := strings.Split(hookName, ":") ex, err := link.OpenExecutable(funcNames[0]) if err != nil { return nil, fmt.Errorf("uretprobe failed to openExecutable binary file %s error %v", hookName, err) } kp, err = ex.Uretprobe(getSymbolName(funcNames), prog, nil) if err != nil { return nil, fmt.Errorf("failed to link uretprobe symbol %s - %v", getSymbolName(funcNames), err) } return kp, nil } func getSymbolName(funcNames []string) string { var symbol string if len(funcNames) == 1 { symbol = funcNames[0] } else { symbol = funcNames[len(funcNames)-1] } return symbol } ================================================ FILE: bpfprogs/probes_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !WINDOWS // +build !WINDOWS package bpfprogs import ( "testing" ) func TestGetProgramSectionDetails(t *testing.T) { tests := []struct { desc string sectionName string want1 string want2 string want3 string }{ { desc: "empty section name", sectionName: "", want1: "", want2: "", }, { desc: "section name contains group", sectionName: "kprobe/perf_event", want1: "kprobe", want2: "perf_event", want3: "", }, { desc: "section name contains all details", sectionName: "tracepoint/sock/inet_sock_set_state", want1: "tracepoint", want2: "sock", want3: "inet_sock_set_state", }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { got1, got2, got3 := GetProgramSectionDetails(test.sectionName) if test.want1 != got1 && test.want2 != got2 && test.want3 != got3 { t.Errorf("want1 %v => got1 %v, want2 %v => got2 %v, want3 %v => got3 %v", test.want1, got1, test.want2, got2, test.want3, got3) } }) } } ================================================ FILE: bpfprogs/processCheck.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package bpfprogs provides primitives for BPF process monitoring. package bpfprogs import ( "container/list" "time" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/rs/zerolog/log" ) type PCheck struct { MaxRetryCount int Chain bool RetryMonitorDelay time.Duration } func NewPCheck(rc int, chain bool, interval time.Duration) *PCheck { c := &PCheck{ MaxRetryCount: rc, Chain: chain, RetryMonitorDelay: interval, } return c } func (c *PCheck) PCheckStart(xdpProgs, ingressTCProgs, egressTCProgs map[string]*list.List, probes *list.List, ifaces *map[string]string) { go c.pMonitorWorker(xdpProgs, models.XDPIngressType, ifaces) go c.pMonitorWorker(ingressTCProgs, models.IngressType, ifaces) go c.pMonitorWorker(egressTCProgs, models.EgressType, ifaces) go c.pMonitorProbeWorker(probes) } func (c *PCheck) pMonitorWorker(bpfProgs map[string]*list.List, direction string, ifaces *map[string]string) { for range time.NewTicker(c.RetryMonitorDelay).C { if models.IsReadOnly { log.Info().Msgf("Not monitoring because we are in readonly state") return } for ifaceName, bpfList := range bpfProgs { if bpfList == nil { // no bpf programs are running continue } for e := bpfList.Front(); e != nil; e = e.Next() { bpf := e.Value.(*BPF) if c.Chain && bpf.Program.SeqID == 0 { // do not monitor root program continue } if bpf.Program.AdminStatus == models.Disabled { continue } userProgram, bpfProgram, _ := bpf.isRunning() if userProgram && bpfProgram { stats.SetWithVersion(1.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, direction, ifaceName, (*ifaces)[ifaceName]) continue } // Not running trying to restart if bpf.RestartCount < c.MaxRetryCount && bpf.Program.AdminStatus == models.Enabled { bpf.RestartCount++ log.Warn().Msgf("pMonitor BPF Program is not running. Restart attempt: %d, program name: %s, iface: %s", bpf.RestartCount, bpf.Program.Name, ifaceName) // User program is a daemon and not running, but the BPF program is loaded if !userProgram && bpfProgram { if err := bpf.StartUserProgram(ifaceName, direction, c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorWorker: BPF Program start user program failed for program %s", bpf.Program.Name) } } // BPF program is not loaded. // if user program is daemon then stop it and restart both the programs if !bpfProgram { log.Warn().Msgf("%s BPF program is not loaded, %s program reloading ...", bpf.Program.EntryFunctionName, bpf.Program.Name) // User program is a daemon and running, stop before reloading the BPF program if bpf.Program.UserProgramDaemon && userProgram { if err := bpf.Stop(ifaceName, (*ifaces)[ifaceName], direction, c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorWorker: BPF Program stop failed for program %s", bpf.Program.Name) } } if err := bpf.Start(ifaceName, (*ifaces)[ifaceName], direction, c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorWorker: BPF Program start failed for program %s", bpf.Program.Name) } } } stats.SetWithVersion(0.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, direction, ifaceName, (*ifaces)[ifaceName]) } } } } func (c *PCheck) pMonitorProbeWorker(bpfProgs *list.List) { for range time.NewTicker(c.RetryMonitorDelay).C { if bpfProgs == nil { time.Sleep(time.Second) continue } for e := bpfProgs.Front(); e != nil; e = e.Next() { bpf := e.Value.(*BPF) if bpf.Program.AdminStatus == models.Disabled { continue } userProgram, bpfProgram, _ := bpf.isRunning() if userProgram && bpfProgram { stats.SetWithVersion(1.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, "", "", "") continue } // Not running trying to restart if bpf.RestartCount < c.MaxRetryCount && bpf.Program.AdminStatus == models.Enabled { bpf.RestartCount++ log.Warn().Msgf("pMonitorProbeWorker: BPF Program is not running. Restart attempt: %d, program name: %s, iface: %s", bpf.RestartCount, bpf.Program.Name, "") // User program is a daemon and not running, but the BPF program is loaded if !userProgram && bpfProgram { if err := bpf.StartUserProgram("", "", c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program start user program failed for program %s", bpf.Program.Name) } } // BPF program is not loaded. // if user program is daemon then stop it and restart both the programs if !bpfProgram { log.Warn().Msgf("%s BPF program is not loaded, %s program reloading ...", bpf.Program.EntryFunctionName, bpf.Program.Name) // User program is a daemon and running, stop before reloading the BPF program if bpf.Program.UserProgramDaemon && userProgram { if err := bpf.Stop("", "", "", c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program stop failed for program %s", bpf.Program.Name) } } if err := bpf.Start("", "", "", c.Chain); err != nil { log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program start failed for program %s", bpf.Program.Name) } } } else { stats.SetWithVersion(0.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, "", "", "") } } } } ================================================ FILE: bpfprogs/processCheck_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package bpfprogs import ( "container/list" "reflect" "testing" "time" ) func TestNewpCheck(t *testing.T) { type args struct { rc int chain bool interval time.Duration } tests := []struct { name string args args want *PCheck wantErr bool }{ { name: "EmptypCheck", args: args{rc: 0, chain: false, interval: 0}, want: &PCheck{MaxRetryCount: 0}, wantErr: false, }, { name: "ValidpCheck", args: args{rc: 3, chain: true, interval: 10}, want: &PCheck{MaxRetryCount: 3}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := NewPCheck(tt.args.rc, false, 0) if !reflect.DeepEqual(got, tt.want) { t.Errorf("NewpCheck() = %v, want %v", got, tt.want) } }) } } func Test_pCheck_pCheckStart(t *testing.T) { type fields struct { MaxRetryCount int chain bool retryMonitorDelay time.Duration } type args struct { IngressXDPbpfProgs map[string]*list.List IngressTCbpfProgs map[string]*list.List EgressTCbpfProgs map[string]*list.List Probebpfs list.List Ifaces map[string]string } tests := []struct { name string fields fields args args wantErr bool }{ { name: "EmptyBPF", fields: fields{MaxRetryCount: 3, chain: true, retryMonitorDelay: 10}, args: args{IngressXDPbpfProgs: make(map[string]*list.List), IngressTCbpfProgs: make(map[string]*list.List), EgressTCbpfProgs: make(map[string]*list.List), Ifaces: make(map[string]string), }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &PCheck{ MaxRetryCount: tt.fields.MaxRetryCount, Chain: tt.fields.chain, RetryMonitorDelay: tt.fields.retryMonitorDelay, } c.PCheckStart(tt.args.IngressXDPbpfProgs, tt.args.IngressTCbpfProgs, tt.args.EgressTCbpfProgs, &tt.args.Probebpfs, &tt.args.Ifaces) }) } } ================================================ FILE: build-docker/Dockerfile ================================================ FROM ubuntu:jammy@sha256:6d7b5d3317a71adb5e175640150e44b8b9a9401a7dd394f44840626aff9fa94d ARG DEBIAN_FRONTEND=noninteractive USER root # Install necessary dependencies RUN apt-get update && \ apt-get install -y \ clang \ llvm \ libelf-dev \ linux-headers-generic \ linux-tools-common linux-tools-generic \ libbpf-dev \ tzdata \ cmake \ zlib1g-dev \ libevent-dev \ vim \ wget \ curl \ linux-tools-generic \ net-tools \ iproute2 \ elfutils \ libjson-c-dev \ curl \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p /srv/l3afd/ RUN mkdir -p /var/l3afd RUN mkdir -p /var/log/l3af RUN mkdir -p /usr/local/l3afd/latest RUN mkdir -p /usr/local/l3afd/v2.1.0/l3afd COPY l3afd /usr/local/l3afd/v2.1.0/l3afd/l3afd COPY l3afd.cfg /usr/local/l3afd/v2.1.0/l3afd/l3afd.cfg COPY start.sh /usr/local/l3afd/start.sh RUN chmod +x /usr/local/l3afd/start.sh RUN ln -s /usr/local/l3afd/v2.1.0/l3afd/l3afd /usr/local/l3afd/latest/l3afd RUN ln -s /usr/local/l3afd/v2.1.0/l3afd/l3afd.cfg /usr/local/l3afd/latest/l3afd.cfg ENTRYPOINT ["/bin/bash","/usr/local/l3afd/start.sh"] ================================================ FILE: build-docker/start.sh ================================================ #!/bin/bash # Start 'l3afd' in the background /usr/local/l3afd/latest/l3afd --config /usr/local/l3afd/latest/l3afd.cfg & sleep 5; function check_and_wait() { sleep_time=$1 while true; do if ps -p $(cat /var/run/l3afd.pid) > /dev/null; then sleep $sleep_time else break fi done } function on_term { echo "Signal $1 received" kill -SIGTERM $(cat /var/run/l3afd.pid) check_and_wait 1 echo "L3AFd process has terminated" } trap 'on_term SIGHUP' SIGHUP trap 'on_term SIGINT' SIGINT trap 'on_term SIGQUIT' SIGQUIT trap 'on_term SIGTERM' SIGTERM trap 'on_term SIGSTOP' SIGSTOP trap 'on_term SIGSEGV' SIGSEGV trap 'on_term SIGILL' SIGILL trap 'on_term SIGKILL' SIGKILL trap 'on_term SIGABRT' SIGABRT trap 'on_term SIGBUS' SIGBUS check_and_wait 5 ================================================ FILE: config/config.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package config provides primitives for l3afd configuration ( i.e. l3afd.cfg) file. package config import ( "crypto/tls" "fmt" "strings" "time" "github.com/robfig/config" "github.com/rs/zerolog/log" ) const ( ENV_PROD = "PROD" ) type Config struct { PIDFilename string DataCenter string BPFDir string BPFLogDir string MinKernelMajorVer int MinKernelMinorVer int EBPFRepoURL string HttpClientTimeout time.Duration MaxEBPFReStartCount int Environment string BpfMapDefaultPath string // Flag to enable chaining with root program BpfChainingEnabled bool FileLogLocation string FileLogMaxSize int FileLogMaxBackups int FileLogMaxAge int JSONFormatLogs bool // stats // Prometheus endpoint for pull/scrape the metrics. MetricsAddr string EBPFPollInterval time.Duration NMetricSamples int ShutdownTimeout time.Duration SwaggerApiEnabled bool // XDP Root program details. XDPRootPackageName string XDPRootArtifact string XDPRootMapName string XDPRootCommand string XDPRootVersion string XDPRootObjectFile string XDPRootEntryFunctionName string // TC Root program details. TCRootPackageName string TCRootArtifact string TCRootIngressMapName string TCRootEgressMapName string TCRootCommand string TCRootVersion string TCRootIngressObjectFile string TCRootEgressObjectFile string TCRootIngressEntryFunctionName string TCRootEgressEntryFunctionName string // ebpf chain details EBPFChainDebugAddr string EBPFChainDebugEnabled bool // l3af configs to listen addrs L3afConfigsRestAPIAddr string // l3af config store L3afConfigStoreFileName string // mTLS MTLSEnabled bool MTLSMinVersion uint16 MTLSCertDir string MTLSCACertFilename string MTLSServerCertFilename string MTLSServerKeyFilename string MTLSCertExpiryWarningDays int MTLSSANMatchRules []string // graceful-restart TimetoRestart int BasePath string RestartArtifactURL string VersionLimit int } // ReadConfig - Initializes configuration from file func ReadConfig(configPath string) (*Config, error) { log.Info().Msgf("Reading configuration from: %s", configPath) confReader, configErr := config.ReadDefault(configPath) if configErr != nil { log.Fatal().Err(configErr).Msgf("Could not open config file %q", configPath) } minTLSVersion, err := loadTLSVersion(confReader, "min-tls-version") if err != nil { return nil, err } return &Config{ PIDFilename: LoadConfigString(confReader, "l3afd", "pid-file"), DataCenter: LoadConfigString(confReader, "l3afd", "datacenter"), BPFDir: LoadConfigString(confReader, "l3afd", "bpf-dir"), BPFLogDir: LoadOptionalConfigString(confReader, "l3afd", "bpf-log-dir", ""), MinKernelMajorVer: LoadOptionalConfigInt(confReader, "l3afd", "kernel-major-version", 5), MinKernelMinorVer: LoadOptionalConfigInt(confReader, "l3afd", "kernel-minor-version", 15), FileLogLocation: LoadOptionalConfigString(confReader, "l3afd", "file-log-location", ""), FileLogMaxSize: LoadOptionalConfigInt(confReader, "l3afd", "file-log-max-size", 100), FileLogMaxBackups: LoadOptionalConfigInt(confReader, "l3afd", "file-log-max-backups", 10), FileLogMaxAge: LoadOptionalConfigInt(confReader, "l3afd", "file-log-max-age", 60), JSONFormatLogs: LoadOptionalConfigBool(confReader, "l3afd", "json-format-logs", false), EBPFRepoURL: LoadConfigString(confReader, "ebpf-repo", "url"), HttpClientTimeout: LoadOptionalConfigDuration(confReader, "l3afd", "http-client-timeout", 30*time.Second), MaxEBPFReStartCount: LoadOptionalConfigInt(confReader, "l3afd", "max-ebpf-restart-count", 3), BpfChainingEnabled: LoadConfigBool(confReader, "l3afd", "bpf-chaining-enabled"), MetricsAddr: LoadOptionalConfigString(confReader, "web", "metrics-addr", "localhost:8898"), EBPFPollInterval: LoadOptionalConfigDuration(confReader, "web", "ebpf-poll-interval", 30*time.Second), NMetricSamples: LoadOptionalConfigInt(confReader, "web", "n-metric-samples", 20), ShutdownTimeout: LoadOptionalConfigDuration(confReader, "l3afd", "shutdown-timeout", 25*time.Second), SwaggerApiEnabled: LoadOptionalConfigBool(confReader, "l3afd", "swagger-api-enabled", false), Environment: LoadOptionalConfigString(confReader, "l3afd", "environment", ENV_PROD), BpfMapDefaultPath: LoadConfigString(confReader, "l3afd", "BpfMapDefaultPath"), XDPRootPackageName: loadXDPRootPackageName(confReader), XDPRootArtifact: loadXDPRootArtifact(confReader), XDPRootMapName: loadXDPRootIngressMapName(confReader), XDPRootCommand: loadXDPRootCommand(confReader), XDPRootVersion: loadXDPRootVersion(confReader), XDPRootObjectFile: LoadOptionalConfigString(confReader, "xdp-root", "object-file", "xdp_root.bpf.o"), XDPRootEntryFunctionName: LoadOptionalConfigString(confReader, "xdp-root", "entry-function-name", "xdp_root"), TCRootPackageName: loadTCRootPackageName(confReader), TCRootArtifact: loadTCRootArtifact(confReader), TCRootIngressMapName: loadTCRootIngressMapName(confReader), TCRootEgressMapName: loadTCRootEgressMapName(confReader), TCRootCommand: loadTCRootCommand(confReader), TCRootVersion: loadTCRootVersion(confReader), TCRootIngressObjectFile: LoadOptionalConfigString(confReader, "tc-root", "ingress-object-file", "tc_root_ingress.bpf.o"), TCRootEgressObjectFile: LoadOptionalConfigString(confReader, "tc-root", "egress-object-file", "tc_root_egress.bpf.o"), TCRootIngressEntryFunctionName: LoadOptionalConfigString(confReader, "tc-root", "ingress-entry-function-name", "tc_ingress_root"), TCRootEgressEntryFunctionName: LoadOptionalConfigString(confReader, "tc-root", "egress-entry-function-name", "tc_egress_root"), EBPFChainDebugAddr: LoadOptionalConfigString(confReader, "ebpf-chain-debug", "addr", "localhost:8899"), EBPFChainDebugEnabled: LoadOptionalConfigBool(confReader, "ebpf-chain-debug", "enabled", false), L3afConfigsRestAPIAddr: LoadOptionalConfigString(confReader, "l3af-configs", "restapi-addr", "localhost:53000"), L3afConfigStoreFileName: LoadConfigString(confReader, "l3af-config-store", "filename"), MTLSEnabled: LoadOptionalConfigBool(confReader, "mtls", "enabled", true), MTLSMinVersion: minTLSVersion, MTLSCertDir: LoadOptionalConfigString(confReader, "mtls", "cert-dir", ""), MTLSCACertFilename: LoadOptionalConfigString(confReader, "mtls", "cacert-filename", "ca.pem"), MTLSServerCertFilename: LoadOptionalConfigString(confReader, "mtls", "server-cert-filename", "server.crt"), MTLSServerKeyFilename: LoadOptionalConfigString(confReader, "mtls", "server-key-filename", "server.key"), MTLSCertExpiryWarningDays: LoadOptionalConfigInt(confReader, "mtls", "cert-expiry-warning-days", 30), MTLSSANMatchRules: strings.Split(LoadOptionalConfigString(confReader, "mtls", "san-match-rules", ""), ","), BasePath: LoadOptionalConfigString(confReader, "graceful-restart", "basepath", "/usr/local/l3afd"), TimetoRestart: LoadOptionalConfigInt(confReader, "graceful-restart", "time-to-restart", 7), VersionLimit: LoadOptionalConfigInt(confReader, "graceful-restart", "version-limit", 100), RestartArtifactURL: LoadOptionalConfigString(confReader, "graceful-restart", "restart-artifacts-url", "file:///srv/l3afd"), }, nil } func loadTLSVersion(cfgRdr *config.Config, fieldName string) (uint16, error) { ver := strings.TrimSpace(LoadOptionalConfigString(cfgRdr, "mTLS", fieldName, "TLS_1.3")) switch ver { case "", "Default", "default": return tls.VersionTLS13, nil case "TLS_1.2": return tls.VersionTLS12, nil case "TLS_1.3": return tls.VersionTLS13, nil default: return 0, fmt.Errorf("unsupported TLS version: %s; use: TLS_1.{2,3}", ver) } } func loadXDPRootPackageName(cfgRdr *config.Config) string { xdpRootPackageName := LoadOptionalConfigString(cfgRdr, "xdp-root-program", "name", "") if xdpRootPackageName == "" { xdpRootPackageName = LoadOptionalConfigString(cfgRdr, "xdp-root", "package-name", "xdp-root") } return xdpRootPackageName } func loadXDPRootArtifact(cfgRdr *config.Config) string { xdpRootArtifactName := LoadOptionalConfigString(cfgRdr, "xdp-root-program", "artifact", "") if xdpRootArtifactName == "" { xdpRootArtifactName = LoadOptionalConfigString(cfgRdr, "xdp-root", "artifact", "l3af_xdp_root.tar.gz") } return xdpRootArtifactName } func loadXDPRootIngressMapName(cfgRdr *config.Config) string { xdpRootIngressMapName := LoadOptionalConfigString(cfgRdr, "xdp-root-program", "ingress-map-name", "") if xdpRootIngressMapName == "" { xdpRootIngressMapName = LoadOptionalConfigString(cfgRdr, "xdp-root", "ingress-map-name", "xdp_root_array") } return xdpRootIngressMapName } func loadXDPRootCommand(cfgRdr *config.Config) string { xdpRootCommand := LoadOptionalConfigString(cfgRdr, "xdp-root-program", "command", "") if xdpRootCommand == "" { xdpRootCommand = LoadOptionalConfigString(cfgRdr, "xdp-root", "command", "xdp_root") } return xdpRootCommand } func loadXDPRootVersion(cfgRdr *config.Config) string { xdpRootVersion := LoadOptionalConfigString(cfgRdr, "xdp-root-program", "version", "") if xdpRootVersion == "" { xdpRootVersion = LoadOptionalConfigString(cfgRdr, "xdp-root", "version", "latest") } return xdpRootVersion } func loadTCRootPackageName(cfgRdr *config.Config) string { tcRootPackageName := LoadOptionalConfigString(cfgRdr, "tc-root-program", "name", "") if tcRootPackageName == "" { tcRootPackageName = LoadOptionalConfigString(cfgRdr, "tc-root", "package-name", "tc-root") } return tcRootPackageName } func loadTCRootArtifact(cfgRdr *config.Config) string { tcRootArtifactName := LoadOptionalConfigString(cfgRdr, "tc-root-program", "artifact", "") if tcRootArtifactName == "" { tcRootArtifactName = LoadOptionalConfigString(cfgRdr, "tc-root", "artifact", "l3af_tc_root.tar.gz") } return tcRootArtifactName } func loadTCRootIngressMapName(cfgRdr *config.Config) string { tcRootIngressMapName := LoadOptionalConfigString(cfgRdr, "tc-root-program", "ingress-map-name", "") if tcRootIngressMapName == "" { tcRootIngressMapName = LoadOptionalConfigString(cfgRdr, "tc-root", "ingress-map-name", "tc_ingress_root_array") } return tcRootIngressMapName } func loadTCRootEgressMapName(cfgRdr *config.Config) string { tcRootEgressMapName := LoadOptionalConfigString(cfgRdr, "tc-root-program", "egress-map-name", "") if tcRootEgressMapName == "" { tcRootEgressMapName = LoadOptionalConfigString(cfgRdr, "tc-root", "egress-map-name", "tc_egress_root_array") } return tcRootEgressMapName } func loadTCRootCommand(cfgRdr *config.Config) string { tcRootCommand := LoadOptionalConfigString(cfgRdr, "tc-root-program", "command", "") if tcRootCommand == "" { tcRootCommand = LoadOptionalConfigString(cfgRdr, "tc-root", "command", "tc_root") } return tcRootCommand } func loadTCRootVersion(cfgRdr *config.Config) string { tcRootVersion := LoadOptionalConfigString(cfgRdr, "tc-root-program", "version", "") if tcRootVersion == "" { tcRootVersion = LoadOptionalConfigString(cfgRdr, "tc-root", "version", "latest") } return tcRootVersion } ================================================ FILE: config/config_loader.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package config import ( "encoding/csv" "net/url" "strings" "time" "github.com/robfig/config" "github.com/rs/zerolog/log" ) const ( cfgFatalMsg = "Could not read %s value %q from group %q in config file" cfgOptionalMsg = "Using default value %v after failure to read group:%s; field:%s" ) //Config Shortcuts-------------------------------------------------------------- // Note: For all the LoadXXX functions, we now have an equivalent LoadOptionalXXX variant. // The LoadOptionalXXX variant will accept a default value as a parameter and if it fails to // read in a value from the cfg, will return the default value as opposed to terminating odnd. // For any new parameters that are not essential and/or have reasonable defaults - it will be // a good idea to use the LoadOptionalXXX function. // LoadConfigString gets the value (as a string) for a field belonging to a group. // If the group and field are present - it returns the value // If the group or field are absent - it aborts the process // Note: Values that are encrypted are decrypted using a global key func LoadConfigString(confReader *config.Config, group, field string) string { return LoadConfigStringEncKey(confReader, group, field) } // LoadOptionalConfigString gets the value (as a string) for a field belonging to a group. // If the group and field are present - it returns the value // If the group or field are absent - it returns the supplied default value // Note: Values that are encrypted are decrypted using a global key func LoadOptionalConfigString(confReader *config.Config, group, field, defaultValue string) string { return LoadOptionalConfigStringEncKey(confReader, group, field, defaultValue) } // LoadOptionalConfigStringEncKey is similar to LoadOptionalConfigString, except that it accepts an optional decryption key. func LoadOptionalConfigStringEncKey(confReader *config.Config, group, field, defaultValue string) string { val, err := loadConfigStringEncKey(confReader, group, field) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return val } // LoadConfigStringEncKey is similar to LoadConfigString, except that it accepts an optional decryption key. func LoadConfigStringEncKey(confReader *config.Config, group, field string) string { val, err := loadConfigStringEncKey(confReader, group, field) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "text", field, group) } return val } // loadConfigStringEncKey attempts to read the value for a given group and field in a config object. // If the value is absent - an error is returned to the caller, who can then abort execution or return a default. // If the value is present - it is returned to the caller, and optionally decrypted if the value starts with `ENC:` // Note: Decryption is done with the supplied key. If nil - a global key is used for decryption. func loadConfigStringEncKey(confReader *config.Config, group, field string) (string, error) { //Read value from config reader value, err := confReader.String(group, field) if err != nil { return "", err } return value, nil } func LoadConfigBool(confReader *config.Config, group, field string) bool { value, err := confReader.Bool(group, field) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "logical", field, group) } return value } func LoadOptionalConfigBool(confReader *config.Config, group, field string, defaultValue bool) bool { value, err := confReader.Bool(group, field) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return value } func LoadConfigInt(confReader *config.Config, group, field string) int { value, err := confReader.Int(group, field) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "integer", field, group) } return value } func LoadOptionalConfigInt(confReader *config.Config, group, field string, defaultValue int) int { value, err := confReader.Int(group, field) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return value } func LoadConfigFloat(confReader *config.Config, group, field string) float64 { value, err := confReader.Float(group, field) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "decimal point", field, group) } return value } func LoadOptionalConfigFloat(confReader *config.Config, group, field string, defaultValue float64) float64 { value, err := confReader.Float(group, field) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return value } func LoadConfigDuration(confReader *config.Config, group, field string) time.Duration { value, err := time.ParseDuration(LoadConfigString(confReader, group, field)) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "duration", field, group) } return value } func LoadOptionalConfigDuration(confReader *config.Config, group, field string, defaultValue time.Duration) time.Duration { stringValue := LoadOptionalConfigString(confReader, group, field, "") if len(stringValue) == 0 { return defaultValue } value, err := time.ParseDuration(stringValue) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return value } func LoadConfigURL(confReader *config.Config, group, field string) *url.URL { value, err := url.Parse(LoadConfigString(confReader, group, field)) if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "url", field, group) } return value } func LoadOptionalConfigURL(confReader *config.Config, group, field string, defaultValue *url.URL) *url.URL { stringValue := LoadOptionalConfigString(confReader, group, field, "") if len(stringValue) == 0 { return defaultValue } value, err := url.Parse(stringValue) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } return value } // LoadConfigStringCSV splits a CSV config string value and returns the // resulting slice of strings. An emptyDefault []string is returned if the config // field is emptyDefault (as opposed to []string{""}, which strings.Split() would // return). func LoadConfigStringCSV(confReader *config.Config, group, field string) []string { CSVStr := strings.TrimSpace(LoadConfigString(confReader, group, field)) if CSVStr == "" { return []string{} } vals, err := csv.NewReader(strings.NewReader(CSVStr)).Read() if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "CSV string", field, group) } return vals } func LoadOptionalConfigStringCSV(confReader *config.Config, group, field string, defaultValue []string) []string { val, err := loadConfigStringEncKey(confReader, group, field) if err != nil { log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) return defaultValue } CSVStr := strings.TrimSpace(val) if CSVStr == "" { return []string{} } vals, err := csv.NewReader(strings.NewReader(CSVStr)).Read() if err != nil { log.Fatal().Err(err).Msgf(cfgFatalMsg, "CSV string", field, group) } return vals } ================================================ FILE: config/l3afd.cfg ================================================ [DEFAULT] [l3afd] pid-file: /var/run/l3afd.pid datacenter: dummy bpf-dir: /dev/shm bpf-log-dir: kernel-major-version: 5 kernel-minor-version: 15 shutdown-timeout: 25s http-client-timeout: 10s max-ebpf-restart-count: 3 bpf-chaining-enabled: true swagger-api-enabled: true environment: DEV BpfMapDefaultPath: /sys/fs/bpf #file-log-location: /var/log/l3afd.log #file-log-max-size: 100 #file-log-max-backups: 20 #file-log-max-age: 60 #json-format-logs: true [ebpf-repo] url: file:///srv/l3afd [web] metrics-addr: localhost:8898 ebpf-poll-interval: 30s n-metric-samples: 20 [xdp-root] package-name: xdp-root artifact: l3af_xdp_root.tar.gz command: xdp_root ingress-map-name: xdp_root_array version: latest object-file: xdp_root.bpf.o entry-function-name: xdp_root [tc-root] package-name: tc-root artifact: l3af_tc_root.tar.gz ingress-map-name: tc_ingress_root_array egress-map-name: tc_egress_root_array command: tc_root version: latest ingress-object-file: tc_root_ingress.bpf.o egress-object-file: tc_root_egress.bpf.o ingress-entry-function-name: tc_ingress_root egress-entry-function-name: tc_egress_root [ebpf-chain-debug] addr: localhost:8899 enabled: true [l3af-configs] restapi-addr: localhost:7080 [l3af-config-store] filename: /var/l3afd/l3af-config.json [mtls] enabled: false # TLS_1_2 or TLS_1_3 # min-tls-version: TLS_1_3 # cert-dir: /etc/l3af/certs # cacert-filename: ca.pem # server-crt-filename: server.crt # server-key-filename: server.key # how many days before expiry you want warning # cert-expiry-warning-days: 30 # multiple domains seperated by comma # literal and regex are validated in lowercase # san-match-rules: .+l3afd.l3af.io,.*l3af.l3af.io,^l3afd.l3af.io$ [l3af-config-store] filename: /var/l3afd/l3af-config.json [graceful-restart] restart-artifacts-url: file:///srv/l3afd time-to-restart: 7 basepath: /usr/local/l3afd version-limit: 100 ================================================ FILE: config.yml ================================================ register_admind.go bpfprogs/cdbhelpers.go bpfprogs/bpfCfgs.go ================================================ FILE: docs/CONTRIBUTING.md ================================================ # How To Contribute ## Some Ways to Contribute - [Getting started with L3AF](#getting-started-with-l3af) - [Report potential bugs](#report-potential-bugs) - [New features or product enhancements](#new-features-or-product-enhancements) - [Submitting a patch that fixes a bug](#submitting-a-patch-that-fixes-a-bug) - [Coding style](#coding-style) - [Guidelines for pull requests](#guidelines-for-pull-requests) - [Improve our guides and documentation](#improve-our-guides-and-documentation) - [Increase our test coverage](#increase-our-test-coverage) ### Getting started with L3AF See [L3AF](https://wiki.lfnetworking.org/display/L3AF/Getting+Started+with+L3AF) ### Report potential bugs First, **ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/l3af-project/l3afd/issues). If you did not find a related bug, you can help us by [submitting a GitHub Issue](https://github.com/l3af-project/l3afd/issues/new). A good bug report provides a detailed description of the issue and step-by-step instructions for reliably reproducing the issue. We will aim to triage issues in [weekly TSC meetings](https://wiki.lfnetworking.org/display/L3AF/Community+Meetings). In case we are unable to repro the issue, we will request more information from you. There will be a waiting period of 2 weeks for the requested information and if there is no response, the issue will be closed. If this happens, please reopen the issue if you do get a repro and provide the requested information. If you found a security issue, please do not open a GitHub Issue, and instead [email](mailto:security@lists.l3af.io) it in detail. ### New features or product enhancements You can request or implement a new feature by [submitting a GitHub Issue](https://github.com/l3af-project/l3afd/issues/new). and communicate your proposal so that the L3AF community can review and provide feedback. Getting early feedback will help ensure your implementation work is accepted by the community. This will also allow us to better coordinate our efforts and minimize duplicated effort. ### Submitting a patch that fixes a bug Fork the repo and make your changes. Then open a new GitHub pull request with the patch. * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. ### Guidelines for pull requests Great, you want to directly contribute to the l3af project and submit a pull request. It is recommended prior to working on a PR to submit an issue in github for the change you want to make describing the change and context around it. This gives the l3af maintainers a chance to review the issue and provide feedback and work with you on the change. If you have any questions, please feel free to reach out to the l3af maintainers via [Slack](https://app.slack.com/client/T02GD9YQJUT/C02GRTC0SAD) or [mail](main@lists.l3af.io) group. Below are some general guidelines to help ensure a successful PR approval. - Provide background why you are making the change and the issue it addresses - List what is changing and provide a high-level summary of the change - List any relevant dependencies linked to this change - Describe the tests that you ran to verify your changes and add/update test cases - Update relevant docs, especially if you've changed APIs - Ensure all GitHub CI/CD checks pass ### Coding Style See [uber-go](https://github.com/uber-go/guide/blob/master/style.md) ### Improve our guides and documentation We look forward to contributions improving our guides and documentation. Documentation should be written in an inclusive style. The [Google developer documentation](https://developers.google.com/style/inclusive-documentation) contains an excellent reference on this topic. ### Increase our test coverage Increase the code coverage by adding tests. PRs are expected to have 100% test coverage for added code. This can be verified with a coverage build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. Run your tests and get coverage locally ```bash go test -race -covermode=atomic -coverprofile=coverage.out ``` ## Developer Certificate of Origin (DCO) The [Developer Certificate of Origin](https://developercertificate.org/) is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the project. This App enforces the Developer Certificate of Origin on Pull Requests. It requires all commit messages to contain the ```Signed-off-by``` line with an email address that matches the commit author. ## Governance Please refer to the [governance repo](https://github.com/l3af-project/governance) for Project Charter, Code of Conduct, Release Process, and Committee Members. ================================================ FILE: docs/api/README.md ================================================ # L3AFD API Documentation # Update API See [payload.json](https://github.com/l3af-project/l3af-arch/blob/main/dev_environment/cfg/payload.json) for a full example payload. The payload will look more like this standard JSON: ``` [ { "host_name" : "l3af-local-test", "iface" : "enp0s3", "bpf_programs" : { "xdp_ingress" : [ { "name": "ratelimiting", "seq_id": 1, "artifact": "l3af_ratelimiting.tar.gz", "ebpf_package_repo_url": "https://l3af.io" "map_name": "xdp_rl_ingress_next_prog", "cmd_start": "", "version": "latest", "user_program_daemon": true, "admin_status": "enabled", "prog_type": "xdp", "cfg_version": 1, "map_args": { "rl_ports_map": "8080,8081", "rl_config_map": "2" }, "monitor_maps": [ { "name": "rl_drop_count_map", "key": 0, "aggregator": "scalar"}, { "name": "rl_recv_count_map", "key": 0, "aggregator": "max-rate"} ], "object_file": "ratelimiting.bpf.o", "entry_function_name": "_xdp_ratelimiting" } ], "tc_ingress":[ {"...": "..."} ], "tc_egress": [ {"...": "..."} ] } } ] ``` ### Below is the detailed documentation for each field | Key | Type | Example | Description | |-----------------------|------------------------------------------------|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | name | string | ratelimiting | Name of the BPF Program | | seq_id | number | `1` | Position of the BPF program in the chain. Count starts at 1. | | artifact | string | `"l3af_ratelimiting.tar.gz"` | Userspace BPF program binary and kernel BPF program byte code in tar.gz format | | ebpf_package_repo_url | string | `"https://l3af.io/"` | eBPF package repository URL. If it is not provided default URL is used. | | map_name | string | `"ep1_next_prog_array"` | Chaining program map to pin to. This should match the BPF program code. | | cmd_start | string | `"ratelimiting"` | The command used to start the userspace program. Usually the userspace program binary name. This userspace program can load bpf program (i.e. not loaded by l3afd) and execute custom logic. If the BPF program is loaded by userspace program then initial linking of BPF program should be handled by this program. | | cmd_stop | string | | The command used stop the userspace program. This program should unlink the program and cleanup the BPF maps | | cmd_status | string | | The command used to get the status of the BPF program. | | cmd_update | string | | The command used to start the program to update BPF maps dynamically. Usually the userspace program binary name. | | version | string | `"latest"` | The version of the BPF Program | | user_program_daemon | boolean | `true` or `false` | Whether the userspace program continues running after the BPF program is started | | admin_status | string | `"enabled"` or `"disabled"` | This represents the program status. `"enabled"` means to be started if not running. `"disabled"` means to be stopped if running | | prog_type | string | `"xdp"` or `"tc"` | Type of BPF program. Currently only XDP and TC network programs are supported. | | cfg_version | number | `1` | Payload version number | | start_args | map | `{"collector_ip": "10.10.10.2", "verbose":"2"}` | Argument list passed while starting the userspace program using cmd_start. | | stop_args | map | | Argument list passed while stopping the userspace program using cmd_stop. | | status_args | map | | Argument list passed while checking the running status of the user program using cmd_status. | | map_args | map | `{"rl_config_map": "2", "rl_ports_map":"80,443"}` | BPF map to be updated with the value provided in the config. This option can only be utilized when object file provided to load by l3afd. | | update_args | map | | Argument list passed while calling cmd_update to update the configuration BPF maps. A program must have logic to parse the map argument and update the appropriate configuration maps for the BPF program. | | monitor_maps | array of [monitor_maps](#monitor_maps) objects | `[{"name":"cl_drop_count_map","key":0,"aggregator":"scalar"}]` | The BPF maps to monitor for metrics and how to aggregate metrics information at each interval metrics are sampled. This option can only be utilized when object file provided to load by l3afd. | | object_file | string | `ratelimiting.bpf.o` | The Object file containing BPF programs and maps, this option is needed to load BPF program from l3afd | | entry_function_name | string | `_xdp_ratelimiting` | The BPF program entry function name, this option is needed to load BPF program from l3afd | Note: `name`, `version`, the Linux distribution name, and `artifact` are combined with the configured ebpf-repo URL into the path that is used to download the artifact containing the BPF program. For example, if `name="ratelimiting"`, `version="latest"`, and `artifact="l3af_ratelimiting.tar.gz"` and L3AFD is running on Ubuntu 20.04.3 LTS (Focal Fossa), then we would look for the artifact at: `http://{ebpf-repo configured in l3afd.cfg}/ratelimiting/latest/focal/l3af_ratelimiting.tar.gz` ## monitor_maps |Key|Type|Example|Description| |--- |--- |--- |--- | |name|string|`"rl_drop_count_map"`|The name of the map where metrics are stored| |key|number|0|The index in the map specified by `name` where metrics are stored| |aggregator|string|scalar|The type of metrics aggregation to use for the configured metric sampling interval. Supported values are `"scalar"`, `"max-rate"`, and `"avg"`.| # Add API The JSON is the same as for the Update API. Refer to above documentation. # Delete API See [delete_payload.json](https://github.com/l3af-project/l3af-arch/blob/main/dev_environment/cfg/delete_payload.json) for a full example payload. The payload will look more like this standard JSON: ``` [ { "host_name": "l3af-local-test", "iface": "fakeif0", "bpf_programs": { "xdp_ingress": [ "ratelimiting", "connection-limit" ], "tc_ingress": [ "...", "..." ], "tc_egress": [ "...", "..." ] } } ] ``` ### Below is the detailed documentation for each field | FieldName | Example | Description | | ------------- | ------------- |---------------------------------------| | host_name | `"l3af-local-test"` | The host's name | | iface | `"fakeif0"` | Interface name | | bpf_programs | `""` | List of BPF program names | | xdp_ingress | `""` | Names of xdp ingress type BPF programs | | tc_ingress | `""` | Names of tc ingress type BPF programs | | tc_egress | `""` | Names of tc egress type BPF programs | ================================================ FILE: docs/configdoc.md ================================================ # L3AFD Config Options Documentation See [l3afd.cfg](https://github.com/l3af-project/l3afd/blob/main/config/l3afd.cfg) for a full example configuration. ``` [DEFAULT] [l3afd] pid-file: ./l3afd.pid datacenter: dc bpf-dir: /dev/shm bpf-log-dir: shutdown-timeout: 1s http-client-timeout: 10s max-ebpf-restart-count: 3 bpf-chaining-enabled: true swagger-api-enabled: false # PROD | DEV environment: PROD .... ``` ### Below is the detailed documentation for each field ## [l3afd] | FieldName | Default | Description | Required | | ------------- |--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| --------------- | |pid-file| `"/var/l3afd/l3afd.pid"` | The path to the l3afd.pid file which contains process id of L3afd | Yes | |datacenter| `"dc"` | Name of Datacenter | Yes | |bpf-dir| `"/dev/shm"` | Absolute Path where eBPF packages are to be extracted | Yes | |bpf-log-dir| `""` | Absolute Path for log files, which is passed to applications on the command line. L3afd does not store any logs itself. | No | |kernel-major-version| `"5"` | Major version of the kernel required to run eBPF programs (Linux Only) | No | |kernel-minor-version| `"1"` | Minor version of the kernel required to run eBPF programs (Linux Only) | No | |shutdown-timeout| `"1s"` | Maximum amount of time allowed for l3afd to gracefully stop. After shutdown-timeout, l3afd will exit even if it could not stop applications. | No | |http-client-timeout| `"10s"` | Maximum amount of time allowed to get HTTP response headers when fetching a package from a repository | No | |max-nf-restart-count| `"3"` | Maximum number of tries to restart eBPF applications if they are not running | No | |bpf-chaining-enabled| `"true"` | Boolean to set bpf-chaining. For more info about bpf chaining check [L3AF_KFaaS.pdf](https://github.com/l3af-project/l3af-arch/blob/main/L3AF_KFaaS.pdf) | Yes | |swagger-api-enabled| `"false"` | Whether the swagger API is enabled or not. For more info see [swagger.md](https://github.com/l3af-project/l3afd/blob/main/docs/swagger.md) | No | |environment| `"PROD"` | If set to anything other than "PROD", mTLS security will not be checked | Yes | |BpfMapDefaultPath| `"/sys/fs/bpf"` | The base pin path for eBPF maps | Yes | | file-log-location | `"/var/log/l3afd.log"` | Location of the log file | No | | file-log-max-size | `"100"` | Max size in megabytes for Log file rotation | No | | file-log-max-backups | `"20"` | Max size in megabytes for Log file rotation | No | | file-log-max-age | `"60"` | Max number of days to keep Log files | No | | json-format-logs | `"false"` | Logs to be printed as a JSON object | No | ## [ebpf-repo] | FieldName | Default | Description | Required | | ------------- |----------------------------| --------------- |----------| |url| `"file:///var/l3afd/repo"` |Default repository from which to download eBPF packages| Yes | ## [web] | FieldName | Default | Description | Required | |--------------------| ------------- | --------------- |----------| | metrics-addr |`"0.0.0.0:8898"`|Prometheus endpoint for pulling/scraping the metrics. For more info about Prometheus see [prometheus.io](https://prometheus.io/) | Yes | | ebpf-poll-interval |`"30s"`|Periodic interval at which to scrape metrics using Prometheus| No | | n-metric-samples |`"20"`|Number of Metric Samples| No | ## [xdp-root] This section is needed when bpf-chaining-enabled is set to true. | FieldName | Default | Description | Required | |---------------------|--------------------------|--------------------------------------------------------------------------| --------------- | | package-name | `"xdp-root"` | Name of subdirectory in which to extract artifact | Yes | | artifact | `"l3af_xdp_root.tar.gz"` | Filename of xdp-root package. Only tar.gz and .zip formats are supported | Yes | | ingress-map-name | `"xdp_root_array"` | Ingress map name of xdp-root program | Yes | | command | `"xdp_root"` | Command to run xdp-root program | Yes | | version | `"latest"` | Version of xdp-root program | Yes | | object-file | `"xdp_root.bpf.o"` | File containing the object code for xdp-root program | Yes | | entry-function-name | `"xdp_root"` | Name of the function that begins the XDP-root program | Yes | ## [tc-root] This section is needed when bpf-chaining-enabled is set to true. | FieldName | Default | Description | Required | |-----------------------------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| --------------- | | pakage-name | `"tc-root"` | Name of subdirectory in which to extract artifact | Yes | | artifact | `"l3af_tc_root.tar.gz"` | Filename of tc_root package | Yes | | ingress-map-name | `"tc_ingress_root_array"` | Ingress map name of tc_root program | Yes | | egress-map-name | `"tc_egress_root_array"` | Egress map name of tc_root program,for more info about ingress/egress check [cilium](https://docs.cilium.io/en/v1.9/concepts/ebpf/intro/) | Yes | | command | `"tc_root"` | Command to run tc_root program | Yes | | version | `"latest"` | Version of tc_root program | Yes | | ingress-object-file | `"tc_root_ingress.bpf.o"` | File containing the object code for tc-root ingress program | Yes | | egress-object-file | `"tc_root_egress.bpf.o"` | File containing the object code for tc-root egress program | Yes | | ingress-entry-function-name | `"tc_ingress_root"` | Name of the function that begins the tc-root ingress program | Yes | | egress-entry-function-name | `"tc_egress_root"` | Name of the function that begins the tc-root egress program | Yes | ## [ebpf-chain-debug] | FieldName | Default | Description | Required | |-----------|--------------------|----------------------------------------------------------------|----------| | addr | `"localhost:8899"` | Hostname and Port of chaining debug REST API | No | | enabled | `"false"` | Boolean to check ebpf chaining debug details is enabled or not | No | ## [l3af-configs] | FieldName | Default | Description | Required | | ------------- | ------------- | --------------- |----------| |restapi-addr|`"localhost:53000"`| Hostname and Port of l3af-configs REST API | No | ## [l3af-config-store] | FieldName | Default | Description | Required | | ------------- | ------------- | --------------- | --------------- | |filename|`"/etc/l3afd/l3af-config.json"`|Absolute path of persistent config file where we are storing L3afBPFPrograms objects. For more info see [models](https://github.com/l3af-project/l3afd/blob/main/models/l3afd.go)| Yes | ## [mtls] | FieldName | Default | Description | Required | | ------------- |------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| |enabled| `"true"` | Boolean controlling whether mTLS is enabled or not on the REST API exposed by l3afd | No | |min-tls-version| `"1.3"` | Minimum tls version allowed | No | |cert-dir| `"/etc/l3afd/certs"` | Absolute path of CA certificates. On Linux this points to a filesystem directory, but on Windows it can point to a [certificate store](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/certificate-stores) | No | |server-crt-filename| `"server.crt"` | Server's ca certificate filename | No | |server-key-filename| `"server.key"` | Server's mtls key filename | No | |cert-expiry-warning-days| `"30"` | How many days before expiry you want warning | No | |san-match-rules| `".*l3af.l3af.io,^l3afd.l3af.io$"` | List of domain names (exact match) or regular expressions to validate client SAN DNS Names against | No | ================================================ FILE: docs/docs.go ================================================ // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": {}, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/l3af/configs/v1": { "get": { "description": "Returns details of the configuration of eBPF Programs for all interfaces on a node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Returns details of the configuration of eBPF Programs for all interfaces on a node", "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/add": { "post": { "description": "Adds new eBPF Programs on node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Adds new eBPF Programs on node", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/delete": { "post": { "description": "Removes eBPF Programs on node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Removes eBPF Programs on node", "parameters": [ { "description": "BPF program names", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFProgramNames" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/restart": { "put": { "description": "Store meta data about ebpf programs and exit", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Store meta data about ebpf programs and exit", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/update": { "post": { "description": "Update eBPF Programs configuration", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Update eBPF Programs configuration", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/{iface}": { "get": { "description": "Returns details of the configuration of eBPF Programs for a given interface", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Returns details of the configuration of eBPF Programs for a given interface", "parameters": [ { "type": "string", "description": "interface name", "name": "iface", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" } } } } }, "definitions": { "models.BPFProgram": { "type": "object", "properties": { "admin_status": { "description": "Program admin status enabled or disabled", "type": "string" }, "artifact": { "description": "Artifact file name", "type": "string" }, "cfg_version": { "description": "Config version", "type": "integer" }, "cmd_config": { "description": "Program config providing command", "type": "string" }, "cmd_start": { "description": "Program start command", "type": "string" }, "cmd_status": { "description": "Program status command", "type": "string" }, "cmd_stop": { "description": "Program stop command", "type": "string" }, "cmd_update": { "description": "Program update config command", "type": "string" }, "config_args": { "description": "Map of arguments to config command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "config_file_path": { "description": "Config file location", "type": "string" }, "cpu": { "description": "User program cpu limits", "type": "integer" }, "ebpf_package_repo_url": { "description": "Download url for Program", "type": "string" }, "entry_function_name": { "description": "BPF entry function name to load", "type": "string" }, "id": { "description": "Program id", "type": "integer" }, "is_plugin": { "description": "User program is plugin or not", "type": "boolean" }, "map_args": { "description": "Config BPF Map of arguments", "type": "array", "items": { "$ref": "#/definitions/models.L3afDMapArg" } }, "map_name": { "description": "BPF map to store next program fd", "type": "string" }, "memory": { "description": "User program memory limits", "type": "integer" }, "monitor_maps": { "description": "Metrics BPF maps", "type": "array", "items": { "$ref": "#/definitions/models.L3afDNFMetricsMap" } }, "name": { "description": "Name of the BPF program package", "type": "string" }, "object_file": { "description": "Object file contains BPF code", "type": "string" }, "prog_type": { "description": "Program type XDP or TC", "type": "string" }, "rules": { "description": "Config rules", "type": "string" }, "rules_file": { "description": "Config rules file name", "type": "string" }, "seq_id": { "description": "Sequence position in the chain", "type": "integer" }, "start_args": { "description": "Map of arguments to start command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "status_args": { "description": "Map of arguments to status command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "stop_args": { "description": "Map of arguments to stop command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "update_args": { "description": "Map of arguments to update command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "user_program_daemon": { "description": "User program daemon or not", "type": "boolean" }, "version": { "description": "Program version", "type": "string" } } }, "models.BPFProgramNames": { "type": "object", "properties": { "probes": { "description": "names of the probe eBPF programs", "type": "array", "items": { "type": "string" } }, "tc_egress": { "description": "names of the TC egress eBPF programs", "type": "array", "items": { "type": "string" } }, "tc_ingress": { "description": "names of the TC ingress eBPF programs", "type": "array", "items": { "type": "string" } }, "xdp_ingress": { "description": "names of the XDP ingress eBPF programs", "type": "array", "items": { "type": "string" } } } }, "models.BPFPrograms": { "type": "object", "properties": { "probes": { "description": "list of probe bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "tc_egress": { "description": "list of tc egress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "tc_ingress": { "description": "list of tc ingress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "xdp_ingress": { "description": "list of xdp ingress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } } } }, "models.KeyValue": { "type": "object", "properties": { "key": { "description": "Key", "type": "integer" }, "value": { "description": "Value", "type": "integer" } } }, "models.L3afBPFProgramNames": { "type": "object", "properties": { "bpf_programs": { "description": "List of eBPF program names to remove", "allOf": [ { "$ref": "#/definitions/models.BPFProgramNames" } ] }, "host_name": { "description": "Host name or pod name", "type": "string" }, "iface": { "description": "Interface name", "type": "string" } } }, "models.L3afBPFPrograms": { "type": "object", "properties": { "bpf_programs": { "description": "List of bpf programs", "allOf": [ { "$ref": "#/definitions/models.BPFPrograms" } ] }, "host_name": { "description": "Host name or pod name", "type": "string" }, "iface": { "description": "Interface name", "type": "string" } } }, "models.L3afDMapArg": { "type": "object", "properties": { "args": { "description": "BPF map arguments", "type": "array", "items": { "$ref": "#/definitions/models.KeyValue" } }, "name": { "description": "BPF map name", "type": "string" } } }, "models.L3afDNFArgs": { "type": "object", "additionalProperties": true }, "models.L3afDNFMetricsMap": { "type": "object", "properties": { "aggregator": { "description": "Aggregation function names", "type": "string" }, "key": { "description": "Index of the bpf map", "type": "integer" }, "name": { "description": "BPF map name", "type": "string" } } } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "", BasePath: "/", Schemes: []string{}, Title: "L3AFD APIs", Description: "Configuration APIs to deploy and get the details of the eBPF Programs on the node", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", RightDelim: "}}", } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: docs/graceful-restart-guide.md ================================================ # Guide to L3AFD Graceful Restart ## Prerequisites To begin, ensure that you have a specific folder where the `l3afd` binary and `l3afd.cfg` files are present. By default, this should be in `/usr/local/l3afd/`. ## Directory Structure Firstly, create a directory structure as shown below: ``` /usr/local/l3afd# tree . ├── latest │ ├── l3afd -> /usr/local/l3afd/v2.0.0/l3afd/l3afd │ └── l3afd.cfg -> /usr/local/l3afd/v2.0.0/l3afd/l3afd.cfg ├── start.sh └── v2.0.0 └── l3afd ├── l3afd └── l3afd.cfg ``` L3afd runs a certain version (in this case, v2.0.0), and in the 'latest' folder, it is symlinked. ## Starting the l3afd Service To start the service, run the start.sh script: ``` /usr/local/l3afd# cat start.sh #!/bin/bash /usr/local/l3afd/latest/l3afd --config /usr/local/l3afd/latest/l3afd.cfg & ``` Ensure that the PIDFile in the service file matches the path used in `l3afd.cfg`. This is crucial for systemd to monitor the l3afd PID. ## Upgrading to a New Version To upgrade from v2.0.0 to v2.x.x, follow these steps. L3afd supports HTTP, HTTPS, and file protocols for downloading artifacts and tar.gz or .zip compression formats. Here's an example of how to create an artifact: 1. Create a folder at /srv/l3afd/pkg/. 2. Inside that, create /srv/l3afd/pkg/l3afd. 3. Copy your v2.x.0 binary and cfg file to the above folder. 4. Create a tar gunzip file: `tar -czvf l3afd.tar.gz l3afd` or create a zip file. The artifact can be served from a remote server, local server, or local folder. For a local start, your payload for the restart would look like this: ``` { "hostname": "l3af-test-host", "version": "v2.x.x", } ``` ## Restarting the Service To restart the service, use the following API call: ``` curl -X PUT http://localhost:7080/l3af/configs/v1/restart -d "@restart.json" ``` During this graceful restart, eBPF Programs of type probes and all user programs are restarted. Note: During a restart, the HTTP endpoint will always be active, meaning you can make HTTP requests to that endpoint. However, all write operations (add, remove, modify program configurations) are blocked. If there are any dependent services on user_programs, you should restart them manually after restarting eBPF Programs. Expect minor metric discrepancies during the restart process. ================================================ FILE: docs/prod-deploy-guide.md ================================================ # Guide to use L3AF in production environments ## Installing l3afd Download the latest build artifacts for the last stable release on the l3afd [repo page](../../../) ## Configuring l3afd This guide lists recommendations on how to run l3afd in a production environment. Please see [l3afd.cfg](../config/l3afd.cfg) for a sample configuration. The only secure configuration for production deployments at this time is with mTLS enabled. mTLS is necessary to properly protect the REST API when running in production mode. To securely run l3afd in a production environment please follow the configuration guidelines below. * Make sure `environment: PROD` is set to prevent l3afd starting up in an insecure configuration. * Ensure mTLS is set to `enabled: true` in the configuration. * It is recommended to use TLS version `1.3`. * Do not use self-signed certificates. It is always encouraged to use well-known root certificates to create server certificates and client certificates. * The debug log API should only be enabled and set to listen on localhost when it is required to debug issues with program chaining. The debug log should normally be disabled by setting `enable: false` in the `ebpf-chain-debug` section. * For security reasons, it is not recommended configuring l3afd to point to a public eBPF repository. Instead, configure l3afd to point to a private mirror or local file repository once you have validated and ensured the eBPF programs are safe to run in production. * eBPF repository artifacts are retrieved by joining the following elements to build the complete path: `https://////` or `file:///////`. ## Running l3afd * l3afd on Linux needs to run with the `CAP_SYS_ADMIN` or with the `CAP_BPF`, `CAP_NET_ADMIN`, and `CAP_PERFMON` privileges (newer kernels). Unprivileged users will not have the necessary permissions to load eBPF programs. * l3afd only supports handling the following signals `SIGINT`, `SIGTERM`, which will cause l3afd to perform a clean shut down. * l3afd can be configured through a system manager to start on boot, such as systemd. ================================================ FILE: docs/swagger.json ================================================ { "swagger": "2.0", "info": { "description": "Configuration APIs to deploy and get the details of the eBPF Programs on the node", "title": "L3AFD APIs", "contact": {}, "version": "1.0" }, "basePath": "/", "paths": { "/l3af/configs/v1": { "get": { "description": "Returns details of the configuration of eBPF Programs for all interfaces on a node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Returns details of the configuration of eBPF Programs for all interfaces on a node", "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/add": { "post": { "description": "Adds new eBPF Programs on node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Adds new eBPF Programs on node", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/delete": { "post": { "description": "Removes eBPF Programs on node", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Removes eBPF Programs on node", "parameters": [ { "description": "BPF program names", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFProgramNames" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/restart": { "put": { "description": "Store meta data about ebpf programs and exit", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Store meta data about ebpf programs and exit", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/update": { "post": { "description": "Update eBPF Programs configuration", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Update eBPF Programs configuration", "parameters": [ { "description": "BPF programs", "name": "cfgs", "in": "body", "required": true, "schema": { "type": "array", "items": { "$ref": "#/definitions/models.L3afBPFPrograms" } } } ], "responses": { "200": { "description": "OK" } } } }, "/l3af/configs/v1/{iface}": { "get": { "description": "Returns details of the configuration of eBPF Programs for a given interface", "consumes": [ "application/json" ], "produces": [ "application/json" ], "summary": "Returns details of the configuration of eBPF Programs for a given interface", "parameters": [ { "type": "string", "description": "interface name", "name": "iface", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" } } } } }, "definitions": { "models.BPFProgram": { "type": "object", "properties": { "admin_status": { "description": "Program admin status enabled or disabled", "type": "string" }, "artifact": { "description": "Artifact file name", "type": "string" }, "cfg_version": { "description": "Config version", "type": "integer" }, "cmd_config": { "description": "Program config providing command", "type": "string" }, "cmd_start": { "description": "Program start command", "type": "string" }, "cmd_status": { "description": "Program status command", "type": "string" }, "cmd_stop": { "description": "Program stop command", "type": "string" }, "cmd_update": { "description": "Program update config command", "type": "string" }, "config_args": { "description": "Map of arguments to config command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "config_file_path": { "description": "Config file location", "type": "string" }, "cpu": { "description": "User program cpu limits", "type": "integer" }, "ebpf_package_repo_url": { "description": "Download url for Program", "type": "string" }, "entry_function_name": { "description": "BPF entry function name to load", "type": "string" }, "id": { "description": "Program id", "type": "integer" }, "is_plugin": { "description": "User program is plugin or not", "type": "boolean" }, "map_args": { "description": "Config BPF Map of arguments", "type": "array", "items": { "$ref": "#/definitions/models.L3afDMapArg" } }, "map_name": { "description": "BPF map to store next program fd", "type": "string" }, "memory": { "description": "User program memory limits", "type": "integer" }, "monitor_maps": { "description": "Metrics BPF maps", "type": "array", "items": { "$ref": "#/definitions/models.L3afDNFMetricsMap" } }, "name": { "description": "Name of the BPF program package", "type": "string" }, "object_file": { "description": "Object file contains BPF code", "type": "string" }, "prog_type": { "description": "Program type XDP or TC", "type": "string" }, "rules": { "description": "Config rules", "type": "string" }, "rules_file": { "description": "Config rules file name", "type": "string" }, "seq_id": { "description": "Sequence position in the chain", "type": "integer" }, "start_args": { "description": "Map of arguments to start command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "status_args": { "description": "Map of arguments to status command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "stop_args": { "description": "Map of arguments to stop command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "update_args": { "description": "Map of arguments to update command", "allOf": [ { "$ref": "#/definitions/models.L3afDNFArgs" } ] }, "user_program_daemon": { "description": "User program daemon or not", "type": "boolean" }, "version": { "description": "Program version", "type": "string" } } }, "models.BPFProgramNames": { "type": "object", "properties": { "probes": { "description": "names of the probe eBPF programs", "type": "array", "items": { "type": "string" } }, "tc_egress": { "description": "names of the TC egress eBPF programs", "type": "array", "items": { "type": "string" } }, "tc_ingress": { "description": "names of the TC ingress eBPF programs", "type": "array", "items": { "type": "string" } }, "xdp_ingress": { "description": "names of the XDP ingress eBPF programs", "type": "array", "items": { "type": "string" } } } }, "models.BPFPrograms": { "type": "object", "properties": { "probes": { "description": "list of probe bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "tc_egress": { "description": "list of tc egress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "tc_ingress": { "description": "list of tc ingress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } }, "xdp_ingress": { "description": "list of xdp ingress bpf programs", "type": "array", "items": { "$ref": "#/definitions/models.BPFProgram" } } } }, "models.KeyValue": { "type": "object", "properties": { "key": { "description": "Key", "type": "integer" }, "value": { "description": "Value", "type": "integer" } } }, "models.L3afBPFProgramNames": { "type": "object", "properties": { "bpf_programs": { "description": "List of eBPF program names to remove", "allOf": [ { "$ref": "#/definitions/models.BPFProgramNames" } ] }, "host_name": { "description": "Host name or pod name", "type": "string" }, "iface": { "description": "Interface name", "type": "string" } } }, "models.L3afBPFPrograms": { "type": "object", "properties": { "bpf_programs": { "description": "List of bpf programs", "allOf": [ { "$ref": "#/definitions/models.BPFPrograms" } ] }, "host_name": { "description": "Host name or pod name", "type": "string" }, "iface": { "description": "Interface name", "type": "string" } } }, "models.L3afDMapArg": { "type": "object", "properties": { "args": { "description": "BPF map arguments", "type": "array", "items": { "$ref": "#/definitions/models.KeyValue" } }, "name": { "description": "BPF map name", "type": "string" } } }, "models.L3afDNFArgs": { "type": "object", "additionalProperties": true }, "models.L3afDNFMetricsMap": { "type": "object", "properties": { "aggregator": { "description": "Aggregation function names", "type": "string" }, "key": { "description": "Index of the bpf map", "type": "integer" }, "name": { "description": "BPF map name", "type": "string" } } } } } ================================================ FILE: docs/swagger.md ================================================ ## Swaggo setup Our first task is to install the libraries we are dependent on. Run the following commands from the commandline: ``` go get -u github.com/swaggo/swag/cmd/swag go get -u github.com/swaggo/http-swagger go get -u github.com/alecthomas/template ``` The first two commands install swag and http-swagger respectively: #### swag This library converts Go annotations to Swagger 2.0 docs (swagger.json/swagger.yaml), which are later used by http-swagger to serve the Swagger UI. #### http-swagger This library helps to serve the Swagger UI using the docs generated by swag. The third command is to install template, a fork of Go’s text/template package. This dependency is required in the docs.go file generated by swag, and we’ll see an error while running the application without it. ## Generate Swagger documentation #### Adding annotations in code If you have not added the annotations, follow these docs * https://www.soberkoder.com/swagger-go-api-swaggo/ * https://github.com/swaggo/swag#declarative-comments-format #### Generating Swagger specs (swagger.json and swagger.yaml) From the root folder of `l3afd` run following command ``` swag init -d "./" -g "apis/configwatch.go" ``` ================================================ FILE: docs/swagger.yaml ================================================ basePath: / definitions: models.BPFProgram: properties: admin_status: description: Program admin status enabled or disabled type: string artifact: description: Artifact file name type: string cfg_version: description: Config version type: integer cmd_config: description: Program config providing command type: string cmd_start: description: Program start command type: string cmd_status: description: Program status command type: string cmd_stop: description: Program stop command type: string cmd_update: description: Program update config command type: string config_args: allOf: - $ref: '#/definitions/models.L3afDNFArgs' description: Map of arguments to config command config_file_path: description: Config file location type: string cpu: description: User program cpu limits type: integer ebpf_package_repo_url: description: Download url for Program type: string entry_function_name: description: BPF entry function name to load type: string id: description: Program id type: integer is_plugin: description: User program is plugin or not type: boolean map_args: description: Config BPF Map of arguments items: $ref: '#/definitions/models.L3afDMapArg' type: array map_name: description: BPF map to store next program fd type: string memory: description: User program memory limits type: integer monitor_maps: description: Metrics BPF maps items: $ref: '#/definitions/models.L3afDNFMetricsMap' type: array name: description: Name of the BPF program package type: string object_file: description: Object file contains BPF code type: string prog_type: description: Program type XDP or TC type: string rules: description: Config rules type: string rules_file: description: Config rules file name type: string seq_id: description: Sequence position in the chain type: integer start_args: allOf: - $ref: '#/definitions/models.L3afDNFArgs' description: Map of arguments to start command status_args: allOf: - $ref: '#/definitions/models.L3afDNFArgs' description: Map of arguments to status command stop_args: allOf: - $ref: '#/definitions/models.L3afDNFArgs' description: Map of arguments to stop command update_args: allOf: - $ref: '#/definitions/models.L3afDNFArgs' description: Map of arguments to update command user_program_daemon: description: User program daemon or not type: boolean version: description: Program version type: string type: object models.BPFProgramNames: properties: probes: description: names of the probe eBPF programs items: type: string type: array tc_egress: description: names of the TC egress eBPF programs items: type: string type: array tc_ingress: description: names of the TC ingress eBPF programs items: type: string type: array xdp_ingress: description: names of the XDP ingress eBPF programs items: type: string type: array type: object models.BPFPrograms: properties: probes: description: list of probe bpf programs items: $ref: '#/definitions/models.BPFProgram' type: array tc_egress: description: list of tc egress bpf programs items: $ref: '#/definitions/models.BPFProgram' type: array tc_ingress: description: list of tc ingress bpf programs items: $ref: '#/definitions/models.BPFProgram' type: array xdp_ingress: description: list of xdp ingress bpf programs items: $ref: '#/definitions/models.BPFProgram' type: array type: object models.KeyValue: properties: key: description: Key type: integer value: description: Value type: integer type: object models.L3afBPFProgramNames: properties: bpf_programs: allOf: - $ref: '#/definitions/models.BPFProgramNames' description: List of eBPF program names to remove host_name: description: Host name or pod name type: string iface: description: Interface name type: string type: object models.L3afBPFPrograms: properties: bpf_programs: allOf: - $ref: '#/definitions/models.BPFPrograms' description: List of bpf programs host_name: description: Host name or pod name type: string iface: description: Interface name type: string type: object models.L3afDMapArg: properties: args: description: BPF map arguments items: $ref: '#/definitions/models.KeyValue' type: array name: description: BPF map name type: string type: object models.L3afDNFArgs: additionalProperties: true type: object models.L3afDNFMetricsMap: properties: aggregator: description: Aggregation function names type: string key: description: Index of the bpf map type: integer name: description: BPF map name type: string type: object info: contact: {} description: Configuration APIs to deploy and get the details of the eBPF Programs on the node title: L3AFD APIs version: "1.0" paths: /l3af/configs/v1: get: consumes: - application/json description: Returns details of the configuration of eBPF Programs for all interfaces on a node produces: - application/json responses: "200": description: OK summary: Returns details of the configuration of eBPF Programs for all interfaces on a node /l3af/configs/v1/{iface}: get: consumes: - application/json description: Returns details of the configuration of eBPF Programs for a given interface parameters: - description: interface name in: path name: iface required: true type: string produces: - application/json responses: "200": description: OK summary: Returns details of the configuration of eBPF Programs for a given interface /l3af/configs/v1/add: post: consumes: - application/json description: Adds new eBPF Programs on node parameters: - description: BPF programs in: body name: cfgs required: true schema: items: $ref: '#/definitions/models.L3afBPFPrograms' type: array produces: - application/json responses: "200": description: OK summary: Adds new eBPF Programs on node /l3af/configs/v1/delete: post: consumes: - application/json description: Removes eBPF Programs on node parameters: - description: BPF program names in: body name: cfgs required: true schema: items: $ref: '#/definitions/models.L3afBPFProgramNames' type: array produces: - application/json responses: "200": description: OK summary: Removes eBPF Programs on node /l3af/configs/v1/restart: put: consumes: - application/json description: Store meta data about ebpf programs and exit parameters: - description: BPF programs in: body name: cfgs required: true schema: items: $ref: '#/definitions/models.L3afBPFPrograms' type: array produces: - application/json responses: "200": description: OK summary: Store meta data about ebpf programs and exit /l3af/configs/v1/update: post: consumes: - application/json description: Update eBPF Programs configuration parameters: - description: BPF programs in: body name: cfgs required: true schema: items: $ref: '#/definitions/models.L3afBPFPrograms' type: array produces: - application/json responses: "200": description: OK summary: Update eBPF Programs configuration swagger: "2.0" ================================================ FILE: go.mod ================================================ module github.com/l3af-project/l3afd/v2 go 1.26.0 require ( github.com/cilium/ebpf v0.21.0 github.com/go-chi/chi/v5 v5.2.5 github.com/mitchellh/go-ps v1.0.0 github.com/prometheus/client_golang v1.23.2 github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e github.com/rs/zerolog v1.35.0 github.com/safchain/ethtool v0.7.0 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.4 golang.org/x/sys v0.43.0 // exclude ) require ( github.com/florianl/go-tc v0.4.8 go.uber.org/mock v0.6.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/swaggo/files v1.0.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/florianl/go-tc v0.4.8 h1:hgmakUX1Nm0Ba1I0ZkbUl9CH6HbRwqSiwipnpmYp3Es= github.com/florianl/go-tc v0.4.8/go.mod h1:B8GeOEnmrbOnxZtaCvsYJcgIzzmM8c/AIhtfCZsDj3Q= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa793TP5z5GNAn/VLPzlc0ewzWdeP/25gDfgQ= github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e h1:3/9k/etUfgykjM3Rx8X0echJzo7gNNeND/ubPkqYw1k= github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e/go.mod h1:Zerq1qYbCKtIIU9QgPydffGlpYfZ8KI/si49wuTLY/Q= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is= github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: main.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "encoding/gob" "encoding/json" "errors" "flag" "fmt" "io" "net" "os" "runtime" "strconv" "strings" "sync" "time" "gopkg.in/natefinch/lumberjack.v2" "github.com/l3af-project/l3afd/v2/apis" "github.com/l3af-project/l3afd/v2/apis/handlers" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/pidfile" "github.com/l3af-project/l3afd/v2/restart" "github.com/l3af-project/l3afd/v2/stats" "github.com/l3af-project/l3afd/v2/utils" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) const daemonName = "l3afd" var stateSockPath string func setupLogging(conf *config.Config) { // ConsoleWriter formats the logs for user-readability if conf.JSONFormatLogs { log.Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() } // Set the default Log level zerolog.SetGlobalLevel(zerolog.InfoLevel) log.Info().Msgf("Log level set to %q", zerolog.InfoLevel) if logLevelStr := os.Getenv("L3AF_LOG_LEVEL"); logLevelStr != "" { if logLevel, err := zerolog.ParseLevel(logLevelStr); err != nil { log.Error().Err(err).Msg("Invalid environment-specified log level. Defaulting to INFO.") } else { zerolog.SetGlobalLevel(logLevel) log.Info().Msgf("Log level set to %q via environment variable", logLevel) } } if conf.FileLogLocation != "" { log.Info().Msgf("Saving logs to file: %s", conf.FileLogLocation) saveLogsToFile(conf) return } } func saveLogsToFile(conf *config.Config) { logFileWithRotation := &lumberjack.Logger{ Filename: conf.FileLogLocation, MaxSize: conf.FileLogMaxSize, // Max size in megabytes MaxBackups: conf.FileLogMaxBackups, // Max number of old log files to keep MaxAge: conf.FileLogMaxAge, // Max number of days to keep log files } // Create a multi-writer for stdout and the file multiWriter := zerolog.MultiLevelWriter(os.Stdout, logFileWithRotation) if conf.JSONFormatLogs { log.Logger = log.Output(zerolog.ConsoleWriter{ Out: multiWriter}) } else { log.Logger = zerolog.New(multiWriter).With().Timestamp().Logger() } } func main() { models.CloseForRestart = make(chan struct{}) models.IsReadOnly = false models.CurrentWriteReq = 0 models.StateLock = sync.Mutex{} ctx, cancel := context.WithCancel(context.Background()) defer cancel() //Default Logger (uses user-friendly colored log statements in RFC3339Nano (e.g., 2006-01-02T15:04:05.999999999Z07:00) format) zerolog.TimeFieldFormat = time.RFC3339Nano log.Logger = log.Output(zerolog.ConsoleWriter{ Out: os.Stderr}) log.Info().Msgf("%s started.", daemonName) var confPath string flag.StringVar(&confPath, "config", "config/l3afd.cfg", "config path") flag.Parse() initVersion() conf, err := config.ReadConfig(confPath) if err != nil { log.Fatal().Err(err).Msgf("Unable to parse config %q", confPath) } // Setup logging according to the configuration provided setupLogging(conf) populateVersions(conf) if err = pidfile.CheckPIDConflict(conf.PIDFilename); err != nil { if err = setupForRestartOuter(ctx, conf); err != nil { log.Warn().Msg("Doing Normal Startup") } else { log.Fatal().Err(err).Msgf("The PID file: %s, is in an unacceptable state", conf.PIDFilename) } } if err = pidfile.CreatePID(conf.PIDFilename); err != nil { log.Fatal().Err(err).Msgf("The PID file: %s, could not be created", conf.PIDFilename) } if runtime.GOOS == "linux" { if err = checkKernelVersion(conf); err != nil { log.Fatal().Err(err).Msg("The unsupported kernel version please upgrade") } } if err = registerL3afD(conf); err != nil { log.Error().Err(err).Msg("L3afd registration failed") } ebpfConfigs, err := SetupNFConfigs(ctx, conf) if err != nil { log.Fatal().Err(err).Msg("L3afd failed to start") } t, err := ReadConfigsFromConfigStore(conf) if err != nil { log.Error().Err(err).Msg("L3afd failed to read configs from store") } if t != nil { if err := ebpfConfigs.DeployeBPFPrograms(t); err != nil { log.Error().Err(err).Msg("L3afd failed to deploy persistent configs from store") } } if err := handlers.InitConfigs(ebpfConfigs); err != nil { log.Fatal().Err(err).Msg("L3afd failed to initialise configs") } if conf.EBPFChainDebugEnabled { bpfprogs.SetupBPFDebug(conf.EBPFChainDebugAddr, ebpfConfigs) } <-models.CloseForRestart os.Exit(0) } func SetupNFConfigs(ctx context.Context, conf *config.Config) (*bpfprogs.NFConfigs, error) { // Get Hostname machineHostname, err := os.Hostname() if err != nil { log.Error().Err(err).Msg("Could not get hostname from OS") } // setup Metrics endpoint stats.SetupMetrics(machineHostname, daemonName, conf.MetricsAddr) pMon := bpfprogs.NewPCheck(conf.MaxEBPFReStartCount, conf.BpfChainingEnabled, conf.EBPFPollInterval) bpfM := bpfprogs.NewpBpfMetrics(conf.BpfChainingEnabled, conf.NMetricSamples) nfConfigs, err := bpfprogs.NewNFConfigs(ctx, machineHostname, conf, pMon, bpfM) if err != nil { return nil, fmt.Errorf("error in NewNFConfigs setup: %v", err) } if err := apis.StartConfigWatcher(ctx, machineHostname, daemonName, conf, nfConfigs); err != nil { return nil, fmt.Errorf("error in version announcer: %v", err) } return nfConfigs, nil } func checkKernelVersion(conf *config.Config) error { const minVerLen = 2 kernelVersion, err := utils.GetKernelVersion() if err != nil { return fmt.Errorf("failed to find kernel version: %v", err) } //validate version ver := strings.Split(kernelVersion, ".") if len(ver) < minVerLen { return fmt.Errorf("expected minimum kernel version length %d and got %d, ver %+q", minVerLen, len(ver), ver) } major_ver, err := strconv.Atoi(ver[0]) if err != nil { return fmt.Errorf("failed to find kernel major version: %v", err) } minor_ver, err := strconv.Atoi(ver[1]) if err != nil { return fmt.Errorf("failed to find kernel minor version: %v", err) } if major_ver > conf.MinKernelMajorVer { return nil } if major_ver == conf.MinKernelMajorVer && minor_ver >= conf.MinKernelMinorVer { return nil } return fmt.Errorf("expected Kernel version >= %d.%d", conf.MinKernelMajorVer, conf.MinKernelMinorVer) } func ReadConfigsFromConfigStore(conf *config.Config) ([]models.L3afBPFPrograms, error) { // check for persistent file if _, err := os.Stat(conf.L3afConfigStoreFileName); errors.Is(err, os.ErrNotExist) { log.Warn().Msgf("no persistent config exists") return nil, nil } file, err := os.OpenFile(conf.L3afConfigStoreFileName, os.O_RDONLY, os.ModePerm) defer func() { _ = file.Close() }() if err != nil { return nil, fmt.Errorf("failed to open persistent file (%s): %v", conf.L3afConfigStoreFileName, err) } byteValue, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("failed to read persistent file (%s): %v", conf.L3afConfigStoreFileName, err) } var t []models.L3afBPFPrograms if err = json.Unmarshal(byteValue, &t); err != nil { return nil, fmt.Errorf("failed to unmarshal persistent config json: %v", err) } return t, nil } // setupForRestartOuter is a wrapper for setupForRestart, written for better error handling func setupForRestartOuter(ctx context.Context, conf *config.Config) error { if _, err := os.Stat(models.HostSock); os.IsNotExist(err) { return err } stateSockPath = models.StateSock models.IsReadOnly = true err := setupForRestart(ctx, conf) if err != nil { sendState("Failed") log.Fatal().Err(err).Msg("unable to restart the l3afd") } sendState("Ready") models.IsReadOnly = false <-models.CloseForRestart os.Exit(0) return nil } // setupForRestart will start the l3afd with state provided by other l3afd instance func setupForRestart(ctx context.Context, conf *config.Config) error { conn, err := net.Dial("unix", models.HostSock) if err != nil { return fmt.Errorf("unable to dial unix domain socket : %w", err) } decoder := gob.NewDecoder(conn) var t models.L3AFALLHOSTDATA err = decoder.Decode(&t) if err != nil { conn.Close() return fmt.Errorf("unable to decode") } conn.Close() machineHostname, err := os.Hostname() if err != nil { return fmt.Errorf("unable to fetch the hostname") } l, err := restart.GetNetListener(3, "stat_server") if err != nil { return fmt.Errorf("getting stat_server listener failed") } models.AllNetListeners.Store("stat_http", l) l, err = restart.GetNetListener(4, "main_server") if err != nil { return fmt.Errorf("getting main_server listener failed") } models.AllNetListeners.Store("main_http", l) if conf.EBPFChainDebugEnabled { l, err = restart.GetNetListener(5, "debug_server") if err != nil { return fmt.Errorf("getting main_server listener failed") } models.AllNetListeners.Store("debug_http", l) } // setup Metrics endpoint stats.SetupMetrics(machineHostname, daemonName, conf.MetricsAddr) restart.SetMetrics(t) pMon := bpfprogs.NewPCheck(conf.MaxEBPFReStartCount, conf.BpfChainingEnabled, conf.EBPFPollInterval) bpfM := bpfprogs.NewpBpfMetrics(conf.BpfChainingEnabled, conf.NMetricSamples) log.Info().Msgf("Restoring Previous State Graceful Restart") ebpfConfigs, err := restart.Convert(ctx, t, conf) ebpfConfigs.BpfMetricsMon = bpfM ebpfConfigs.ProcessMon = pMon if err != nil { return fmt.Errorf("failed to convert deserilaze the state") } err = ebpfConfigs.StartAllUserProgramsAndProbes() if err != nil { return fmt.Errorf("failed to start all the user programs and probes") } err = apis.StartConfigWatcher(ctx, machineHostname, daemonName, conf, ebpfConfigs) if err != nil { return fmt.Errorf("starting config Watcher failed") } err = handlers.InitConfigs(ebpfConfigs) if err != nil { return fmt.Errorf("l3afd failed to initialise configs") } if conf.EBPFChainDebugEnabled { bpfprogs.SetupBPFDebug(conf.EBPFChainDebugAddr, ebpfConfigs) } ebpfConfigs.ProcessMon.PCheckStart(ebpfConfigs.IngressXDPBpfs, ebpfConfigs.IngressTCBpfs, ebpfConfigs.EgressTCBpfs, &ebpfConfigs.ProbesBpfs, &ebpfConfigs.Ifaces) ebpfConfigs.BpfMetricsMon.BpfMetricsStart(ebpfConfigs.IngressXDPBpfs, ebpfConfigs.IngressTCBpfs, ebpfConfigs.EgressTCBpfs, &ebpfConfigs.ProbesBpfs, &ebpfConfigs.Ifaces) err = pidfile.CreatePID(conf.PIDFilename) if err != nil { return fmt.Errorf("the PID file: %s, could not be created", conf.PIDFilename) } return nil } func sendState(s string) { ln, err := net.Listen("unix", stateSockPath) if err != nil { log.Err(err) os.Exit(0) return } conn, err := ln.Accept() if err != nil { log.Err(err) ln.Close() os.Exit(0) return } encoder := gob.NewEncoder(conn) err = encoder.Encode(s) if err != nil { log.Err(err) conn.Close() ln.Close() os.Exit(0) return } conn.Close() ln.Close() } // populateVersions is to suppress codeql warning - Uncontrolled data used in network request func populateVersions(conf *config.Config) { models.AvailableVersions = make(map[string]string) for i := 0; i <= conf.VersionLimit; i++ { for j := 0; j <= conf.VersionLimit; j++ { for k := 0; k <= conf.VersionLimit; k++ { version := fmt.Sprintf("v%d.%d.%d", i, j, k) models.AvailableVersions[version] = version } } } } ================================================ FILE: main_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package main import ( "os" "testing" "github.com/l3af-project/l3afd/v2/config" ) func TestTestConfigValid(t *testing.T) { // skip if file DNE f, err := os.Open("config/l3afd.cfg") if err != nil { t.Skip("l3afd.cfg not found") } f.Close() if _, err := config.ReadConfig("config/l3afd.cfg"); err != nil { t.Errorf("Unable to read l3afd config: %s", err) } } ================================================ FILE: mocks/mocked_interfaces.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: mock_interfaces.go // Package mocks is a generated GoMock package. package mocks import ( reflect "reflect" gomock "go.uber.org/mock/gomock" ) // MockplatformInterface is a mock of platformInterface interface. type MockplatformInterface struct { ctrl *gomock.Controller recorder *MockplatformInterfaceMockRecorder } // MockplatformInterfaceMockRecorder is the mock recorder for MockplatformInterface. type MockplatformInterfaceMockRecorder struct { mock *MockplatformInterface } // NewMockplatformInterface creates a new mock instance. func NewMockplatformInterface(ctrl *gomock.Controller) *MockplatformInterface { mock := &MockplatformInterface{ctrl: ctrl} mock.recorder = &MockplatformInterfaceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockplatformInterface) EXPECT() *MockplatformInterfaceMockRecorder { return m.recorder } // GetPlatform mocks base method. func (m *MockplatformInterface) GetPlatform() (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPlatform") ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetPlatform indicates an expected call of GetPlatform. func (mr *MockplatformInterfaceMockRecorder) GetPlatform() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatform", reflect.TypeOf((*MockplatformInterface)(nil).GetPlatform)) } ================================================ FILE: models/l3afd.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package models import ( "sync" ) // l3afd constants const ( Enabled = "enabled" Disabled = "disabled" StartType = "start" StopType = "stop" UpdateType = "update" XDPType = "xdp" TCType = "tc" IngressType = "ingress" EgressType = "egress" XDPIngressType = "xdpingress" TCMapPinPath = "tc/globals" KProbe = "kprobe" TracePoint = "tracepoint" KRetProbe = "kretprobe" UProbe = "uprobe" URetProbe = "uretprobe" ) type L3afDNFArgs map[string]interface{} // BPFProgram defines BPF Program for specific host type BPFProgram struct { ID int `json:"id"` // Program id Name string `json:"name"` // Name of the BPF program package SeqID int `json:"seq_id"` // Sequence position in the chain Artifact string `json:"artifact"` // Artifact file name MapName string `json:"map_name"` // BPF map to store next program fd CmdStart string `json:"cmd_start"` // Program start command CmdStop string `json:"cmd_stop"` // Program stop command CmdStatus string `json:"cmd_status"` // Program status command CmdConfig string `json:"cmd_config"` // Program config providing command CmdUpdate string `json:"cmd_update"` // Program update config command Version string `json:"version"` // Program version UserProgramDaemon bool `json:"user_program_daemon"` // User program daemon or not IsPlugin bool `json:"is_plugin"` // User program is plugin or not CPU int `json:"cpu"` // User program cpu limits Memory int `json:"memory"` // User program memory limits AdminStatus string `json:"admin_status"` // Program admin status enabled or disabled ProgType string `json:"prog_type"` // Program type XDP or TC RulesFile string `json:"rules_file"` // Config rules file name Rules string `json:"rules"` // Config rules ConfigFilePath string `json:"config_file_path"` // Config file location CfgVersion int `json:"cfg_version"` // Config version StartArgs L3afDNFArgs `json:"start_args"` // Map of arguments to start command StopArgs L3afDNFArgs `json:"stop_args"` // Map of arguments to stop command StatusArgs L3afDNFArgs `json:"status_args"` // Map of arguments to status command UpdateArgs L3afDNFArgs `json:"update_args"` // Map of arguments to update command MapArgs []L3afDMapArg `json:"map_args"` // Config BPF Map of arguments ConfigArgs L3afDNFArgs `json:"config_args"` // Map of arguments to config command MonitorMaps []L3afDNFMetricsMap `json:"monitor_maps"` // Metrics BPF maps EPRURL string `json:"ebpf_package_repo_url"` // Download url for Program ObjectFile string `json:"object_file"` // Object file contains BPF code EntryFunctionName string `json:"entry_function_name"` // BPF entry function name to load } // L3afDNFMetricsMap defines BPF map type L3afDNFMetricsMap struct { Name string `json:"name"` // BPF map name Key int `json:"key"` // Index of the bpf map Aggregator string `json:"aggregator"` // Aggregation function names } // KeyValue defines struct for key and value type KeyValue struct { Key int `json:"key"` // Key Value int `json:"value"` // Value } // L3afDMapArg defines map arg type L3afDMapArg struct { Name string `json:"name"` // BPF map name Args []KeyValue `json:"args"` // BPF map arguments } // L3afBPFPrograms defines configs for a node type L3afBPFPrograms struct { HostName string `json:"host_name"` // Host name or pod name Iface string `json:"iface"` // Interface name IPv4Address string `json:"ipv4_address"` // Ipv4 address of network interface BpfPrograms *BPFPrograms `json:"bpf_programs"` // List of bpf programs } // BPFPrograms for a node type BPFPrograms struct { XDPIngress []*BPFProgram `json:"xdp_ingress"` // list of xdp ingress bpf programs TCIngress []*BPFProgram `json:"tc_ingress"` // list of tc ingress bpf programs TCEgress []*BPFProgram `json:"tc_egress"` // list of tc egress bpf programs Probes []*BPFProgram `json:"probes"` // list of probe bpf programs } // L3afBPFProgramNames defines names of Bpf programs on interface type L3afBPFProgramNames struct { HostName string `json:"host_name"` // Host name or pod name Iface string `json:"iface"` // Interface name IPv4Address string `json:"ipv4_address"` // Ipv4 address of network interface BpfProgramNames *BPFProgramNames `json:"bpf_programs"` // List of eBPF program names to remove } // BPFProgramNames defines names of eBPF programs on node type BPFProgramNames struct { XDPIngress []string `json:"xdp_ingress"` // names of the XDP ingress eBPF programs TCIngress []string `json:"tc_ingress"` // names of the TC ingress eBPF programs TCEgress []string `json:"tc_egress"` // names of the TC egress eBPF programs Probes []string `json:"probes"` // names of the probe eBPF programs } type MetaColl struct { Programs []string Maps []string } type MetaMetricsBPFMap struct { MapName string Key int Values []float64 Aggregator string LastValue float64 } type Label struct { Name string Value string } type MetricVec struct { MetricName string Labels []Label Value float64 Type int32 } type L3AFMetaData struct { Program BPFProgram FilePath string RestartCount int PrevMapNamePath string MapNamePath string ProgID uint32 BpfMaps []string MetricsBpfMaps map[string]MetaMetricsBPFMap ProgMapCollection MetaColl ProgMapID uint32 PrevProgMapID uint32 Link bool } type L3AFALLHOSTDATA struct { HostName string HostInterfaces map[string]bool IngressXDPBpfs map[string][]*L3AFMetaData IngressTCBpfs map[string][]*L3AFMetaData EgressTCBpfs map[string][]*L3AFMetaData ProbesBpfs []L3AFMetaData Ifaces map[string]string AllStats []MetricVec } type RestartConfig struct { HostName string `json:"hostname"` Version string `json:"version"` } var CloseForRestart chan struct{} var AllNetListeners sync.Map var CurrentWriteReq int var StateLock sync.Mutex var IsReadOnly bool var AvailableVersions map[string]string const HttpScheme string = "http" const HttpsScheme string = "https" const FileScheme string = "file" const StatusFailed string = "Failed" const StatusReady string = "Ready" // Please Do not make changes in socketpaths because they are means of communication between graceful restarts const HostSock string = "/tmp/l3afd.sock" const StateSock string = "/tmp/l3afstate.sock" const L3AFDRestartArtifactName string = "l3afd.tar.gz" ================================================ FILE: pidfile/pidfile.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package pidfile import ( "fmt" "os" "os/signal" "strconv" "syscall" "time" "github.com/rs/zerolog/log" ) func CheckPIDConflict(pidFilename string) error { log.Info().Msgf("Checking for another already running instance (using PID file \"%s\")...", pidFilename) pidFileContent, err := os.ReadFile(pidFilename) if err != nil { if os.IsNotExist(err) { log.Info().Msgf("OK, no PID file already exists at %s.", pidFilename) return nil } return fmt.Errorf("could not open PID file: %s, please manually remove; error: %v", pidFilename, err) } if len(pidFileContent) < 1 { log.Warn().Msgf("PID file already exists at %s, but it is empty... ignoring.", pidFilename) return nil } oldPIDString := string(pidFileContent) oldPID, err := strconv.Atoi(oldPIDString) if err != nil { return fmt.Errorf("PID file: %s, contained value: %s, which could not be parsed; error: %v", pidFilename, oldPIDString, err) } log.Info().Msgf("Found PID file with PID: %d; checking if it is this process: PID: %d", oldPID, os.Getpid()) if oldPID == os.Getpid() { log.Warn().Msgf("PID file already exists at %s, but it contains the current PID(%d)... ignoring.", pidFilename, oldPID) return nil } log.Info().Msgf("Found PID file with PID: %s; checking if process is running...", oldPIDString) process, err := os.FindProcess(oldPID) if err == nil { //On Linux, if sig is 0, then no signal is sent, but error checking is still performed; //this can be used to check for the existence of a process ID or process group ID. //See: man 2 kill err = process.Signal(syscall.Signal(0)) } if err != nil { log.Info().Msgf("Process was not running, removing PID file.") if err = RemovePID(pidFilename); err != nil { return fmt.Errorf("removal failed, please manually remove; err: %v", err) } return nil } log.Info().Msgf("Process with PID: %s; is running. Comparing process names to ensure it is a true conflict.", oldPIDString) selfProcName, err := os.ReadFile("/proc/self/comm") if err != nil { return fmt.Errorf("could not read this processes command name from the proc filesystem; err: %v", err) } conflictProcName, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", oldPID)) if err != nil { return fmt.Errorf("could not read old processes (PID: %s) command name from the proc filesystem; error: %v", oldPIDString, err) } if string(selfProcName) != string(conflictProcName) { log.Info().Msgf("Old process had command name: %q, not %q, removing PID file.", conflictProcName, selfProcName) if err = RemovePID(pidFilename); err != nil { return fmt.Errorf("removal failed, please manually remove; error: %v", err) } return nil } return fmt.Errorf("a previous instance of this process (%s) is running with ID %s; please shutdown this process before running", selfProcName, oldPIDString) } func CreatePID(pidFilename string) error { PID := os.Getpid() log.Info().Msgf("Writing process ID %d to %s...", PID, pidFilename) if err := os.WriteFile(pidFilename, []byte(strconv.Itoa(PID)), 0640); err != nil { return fmt.Errorf("could not write process ID to file: \"%s\"; error: %v", pidFilename, err) } return nil } func RemovePID(pidFilename string) error { err := os.RemoveAll(pidFilename) if err != nil { err = fmt.Errorf("could not remove PID file: %s; error: %v", pidFilename, err) } return err } func SetupGracefulShutdown(shutdownHandler func() error, shutdownHandlerTimeout time.Duration, pidFilename string) { const defaultShutdownTO = time.Second * 10 if shutdownHandlerTimeout < 1 { log.Warn().Msgf("GracefulShutdown: No shutdown timeout was provided! Using %s.", defaultShutdownTO) shutdownHandlerTimeout = defaultShutdownTO } //We must use a buffered channel or risk missing the signal if we're not //ready to receive when the signal is sent. interruptCh := make(chan os.Signal, 1) signal.Notify(interruptCh, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT) //Start worker that listens for shutdown signal and handles it go func() { interrupt := <-interruptCh exitCode := 0 if shutdownHandler != nil { //Run shutdown handler log.Info().Msgf("GracefulShutdown: Received shutdown signal: %s, waiting for shutdown handler to execute (will timeout after %s)...", interrupt, shutdownHandlerTimeout) handlerDoneCh := make(chan struct{}) go func() { if err := shutdownHandler(); err != nil { log.Error().Err(err).Msgf("GracefulShutdown: Shutdown handler returned error") exitCode = 1 } handlerDoneCh <- struct{}{} }() select { case <-handlerDoneCh: log.Info().Msgf("GracefulShutdown: Shutdown handler execution complete. Shutting down...") case <-time.After(shutdownHandlerTimeout): log.Error().Msgf("GracefulShutdown: Shutdown handler execution timed-out after %s! Shutting down...", shutdownHandlerTimeout) exitCode = 1 } } else { log.Info().Msgf("GracefulShutdown: Received shutdown signal: %s, shutting down...", interrupt) } if pidFilename != "" { if err := RemovePID(pidFilename); err != nil { log.Warn().Err(err).Msgf("Could not cleanup PID file") } } log.Info().Msgf("Shutdown now.") os.Exit(exitCode) }() } ================================================ FILE: register_internal.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !admind // +build !admind // This file is used for walmart internal to register with management server. // We will be removing this file in future. package main import ( "github.com/l3af-project/l3afd/v2/config" "github.com/rs/zerolog/log" ) func registerL3afD(conf *config.Config) error { log.Warn().Msg("Implement custom registration with management server") return nil } ================================================ FILE: restart/restart.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package restart provides primitives for gracefully restarting l3afd package restart import ( "bytes" "container/list" "container/ring" "context" "fmt" "net" "net/url" "os" "path/filepath" "strings" "sync" "github.com/l3af-project/l3afd/v2/bpfprogs" "github.com/l3af-project/l3afd/v2/config" "github.com/l3af-project/l3afd/v2/models" "github.com/l3af-project/l3afd/v2/stats" "github.com/l3af-project/l3afd/v2/utils" "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog/log" ) // convertBPFMap will populate bpf maps from deserialized map names func convertBPFMap(in []string, g *bpfprogs.BPF, output *map[string]bpfprogs.BPFMap, iface string) error { version := utils.ReplaceDotsWithUnderscores(g.Program.Version) for _, v := range in { var pinnedPath string if g.Program.ProgType == models.XDPType { pinnedPath = filepath.Join(g.HostConfig.BpfMapDefaultPath, iface, g.Program.Name, version, v) } else { pinnedPath = filepath.Join(g.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, iface, g.Program.Name, version, v) } m, err := ebpf.LoadPinnedMap(pinnedPath, nil) if err != nil { return err } info, err := m.Info() if err != nil { return err } id, _ := info.ID() (*output)[v] = bpfprogs.BPFMap{ Name: v, MapID: id, Type: info.Type, BPFProg: g, } m.Close() } return nil } // getCollection will populate ebpf Collection from deserialized meta collection object func getCollection(input models.MetaColl, output **ebpf.Collection, b *bpfprogs.BPF, iface string) error { version := utils.ReplaceDotsWithUnderscores(b.Program.Version) for _, v := range input.Programs { progPinPath := utils.ProgPinPath(b.HostConfig.BpfMapDefaultPath, iface, b.Program.Name, version, b.Program.EntryFunctionName, b.Program.ProgType) prog, err := ebpf.LoadPinnedProgram(progPinPath, nil) if err != nil { return err } (*output).Programs[v] = prog } for _, v := range input.Maps { var mapPinPath string if b.Program.ProgType == models.TCType { mapPinPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, iface, b.Program.Name, version, v) } else if b.Program.ProgType == models.XDPType { mapPinPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, iface, b.Program.Name, version, v) } m, err := ebpf.LoadPinnedMap(mapPinPath, nil) if err != nil { return err } (*output).Maps[v] = m } return nil } // getMetricsMaps will populate MetricsBPFMap from deserialized meta metric object func getMetricsMaps(input map[string]models.MetaMetricsBPFMap, b *bpfprogs.BPF, conf *config.Config, output *map[string]*bpfprogs.MetricsBPFMap, iface string) error { if input == nil { return nil } version := utils.ReplaceDotsWithUnderscores(b.Program.Version) for k, v := range input { fg := &bpfprogs.MetricsBPFMap{} var pinnedPath string if b.Program.ProgType == models.XDPType { pinnedPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, iface, b.Program.Name, version, v.MapName) } else { pinnedPath = filepath.Join(b.HostConfig.BpfMapDefaultPath, models.TCMapPinPath, iface, b.Program.Name, version, v.MapName) } m, err := ebpf.LoadPinnedMap(pinnedPath, nil) if err != nil { return err } info, err := m.Info() if err != nil { return err } id, _ := info.ID() fg.BPFMap = bpfprogs.BPFMap{ Name: v.MapName, MapID: id, Type: info.Type, BPFProg: b, } fg.Values = ring.New(conf.NMetricSamples) for _, a := range v.Values { fg.Values.Value = a fg.Values = fg.Values.Next() } fg.Aggregator = v.Aggregator fg.Key = v.Key fg.LastValue = v.LastValue (*output)[k] = fg m.Close() } return nil } // deserializeProgram will deserialize individual program from given *models.L3AFMetaData func deserializeProgram(ctx context.Context, r *models.L3AFMetaData, hostconfig *config.Config, iface, direction string) (*bpfprogs.BPF, error) { g := &bpfprogs.BPF{} g.Program = r.Program g.FilePath = r.FilePath g.RestartCount = r.RestartCount g.MapNamePath = r.MapNamePath g.PrevMapNamePath = r.PrevMapNamePath g.PrevProgMapID = ebpf.MapID(r.PrevProgMapID) g.ProgMapID = ebpf.MapID(r.ProgMapID) g.ProgID = ebpf.ProgramID(r.ProgID) g.Ctx = ctx g.HostConfig = hostconfig g.Done = nil g.BpfMaps = make(map[string]bpfprogs.BPFMap) if err := convertBPFMap(r.BpfMaps, g, &g.BpfMaps, iface); err != nil { return nil, err } g.MetricsBpfMaps = make(map[string]*bpfprogs.MetricsBPFMap) if err := getMetricsMaps(r.MetricsBpfMaps, g, hostconfig, &g.MetricsBpfMaps, iface); err != nil { return nil, fmt.Errorf("metrics maps conversion failed") } g.ProgMapCollection = &ebpf.Collection{ Programs: make(map[string]*ebpf.Program), Maps: make(map[string]*ebpf.Map), } if r.Link { version := utils.ReplaceDotsWithUnderscores(g.Program.Version) var linkPinPath string if g.Program.ProgType == models.XDPType { linkPinPath = utils.LinkPinPath(hostconfig.BpfMapDefaultPath, iface, g.Program.Name, version, g.Program.ProgType) } else { linkPinPath = utils.TCLinkPinPath(hostconfig.BpfMapDefaultPath, iface, g.Program.Name, version, g.Program.ProgType, direction) } var err error g.Link, err = link.LoadPinnedLink(linkPinPath, nil) if err != nil { return nil, err } } if err := getCollection(r.ProgMapCollection, &g.ProgMapCollection, g, iface); err != nil { return nil, err } return g, nil } // GetValueofLabel will query label value from given label array func getValueofLabel(l string, t []models.Label) string { for _, f := range t { if f.Name == l { return f.Value } } return "" } // GetGaugeVecByMetricName will provide CounterVec metrics pointer from metric name func getCountVecByMetricName(name string) *prometheus.CounterVec { switch name { case "l3afd_BPFUpdateCount": return stats.BPFUpdateCount case "l3afd_BPFStartCount": return stats.BPFStartCount case "l3afd_BPFStopCount": return stats.BPFStopCount case "l3afd_BPFUpdateFailedCount": return stats.BPFUpdateFailedCount default: return nil } } // GetGaugeVecByMetricName will provide GaugeVec metrics pointer from metric name func getGaugeVecByMetricName(name string) *prometheus.GaugeVec { switch name { case "l3afd_BPFRunning": return stats.BPFRunning case "l3afd_BPFStartTime": return stats.BPFStartTime case "l3afd_BPFMonitorMap": return stats.BPFMonitorMap default: return nil } } // Convert will produce *bpfprogs.NFConfigs from deserilzed l3afd state func Convert(ctx context.Context, t models.L3AFALLHOSTDATA, hostconfig *config.Config) (*bpfprogs.NFConfigs, error) { D := &bpfprogs.NFConfigs{} D.Ctx = ctx D.HostName = t.HostName D.HostInterfaces = t.HostInterfaces D.Ifaces = t.Ifaces D.IngressXDPBpfs = make(map[string]*list.List) D.IngressTCBpfs = make(map[string]*list.List) D.EgressTCBpfs = make(map[string]*list.List) D.ProbesBpfs = *list.New() D.HostConfig = hostconfig D.Mu = new(sync.Mutex) if t.IngressXDPBpfs != nil { for k, v := range t.IngressXDPBpfs { l := list.New() for _, r := range v { f, err := deserializeProgram(ctx, r, hostconfig, k, models.XDPIngressType) if err != nil { log.Err(err).Msg("Deserialization failed for xdp ingress programs") return nil, err } l.PushBack(f) } D.IngressXDPBpfs[k] = l } } if t.IngressTCBpfs != nil { for k, v := range t.IngressTCBpfs { l := list.New() for _, r := range v { f, err := deserializeProgram(ctx, r, hostconfig, k, models.IngressType) if err != nil { log.Err(err).Msg("Deserialization failed for tc ingress programs") return nil, err } l.PushBack(f) } D.IngressTCBpfs[k] = l } } if t.EgressTCBpfs != nil { for k, v := range t.EgressTCBpfs { l := list.New() for _, r := range v { f, err := deserializeProgram(ctx, r, hostconfig, k, models.EgressType) if err != nil { log.Err(err).Msg("Deserialization failed for tc egress programs") return nil, err } l.PushBack(f) } D.EgressTCBpfs[k] = l } } return D, nil } // SetMetrics will populate all stats from models.L3AFALLHOSTDATA func SetMetrics(t models.L3AFALLHOSTDATA) { for _, f := range t.AllStats { if f.Type == 0 { stats.Add(f.Value, getCountVecByMetricName(f.MetricName), getValueofLabel("ebpf_program", f.Labels), getValueofLabel("direction", f.Labels), getValueofLabel("interface_name", f.Labels), getValueofLabel("ipv4_address", f.Labels)) } else { if len(getValueofLabel("version", f.Labels)) > 0 { stats.SetWithVersion(f.Value, getGaugeVecByMetricName(f.MetricName), getValueofLabel("ebpf_program", f.Labels), getValueofLabel("version", f.Labels), getValueofLabel("direction", f.Labels), getValueofLabel("interface_name", f.Labels), getValueofLabel("ipv4_address", f.Labels)) } else if len(getValueofLabel("map_name", f.Labels)) > 0 { stats.SetValue(f.Value, getGaugeVecByMetricName(f.MetricName), getValueofLabel("ebpf_program", f.Labels), getValueofLabel("map_name", f.Labels), getValueofLabel("interface_name", f.Labels), getValueofLabel("ipv4_address", f.Labels)) } else { stats.Set(f.Value, getGaugeVecByMetricName(f.MetricName), getValueofLabel("ebpf_program", f.Labels), getValueofLabel("direction", f.Labels), getValueofLabel("interface_name", f.Labels), getValueofLabel("ipv4_address", f.Labels)) } } } } // GetNetListener will get tcp listener from provided file descriptor func GetNetListener(fd int, fname string) (*net.TCPListener, error) { file := os.NewFile(uintptr(fd), "DupFd"+fname) l, err := net.FileListener(file) if err != nil { return nil, err } lf, e := l.(*net.TCPListener) if !e { return nil, fmt.Errorf("unable to convert to tcp listener") } file.Close() return lf, nil } // AddSymlink to add symlink func AddSymlink(sPath, symlink string) error { err := os.Symlink(sPath, symlink) return err } // RemoveSymlink to remove symlink func RemoveSymlink(symlink string) error { err := os.Remove(symlink) return err } // ReadSymlink to read symlink func ReadSymlink(symlink string) (string, error) { originalPath, err := os.Readlink(symlink) if err != nil { return "", err } return originalPath, nil } // GetNewVersion will download new version make it ready to execute func GetNewVersion(artifactName, oldVersion, newVersion string, conf *config.Config) error { if oldVersion == newVersion { return nil } newVersionPath := filepath.Clean(filepath.Join(conf.BasePath, newVersion)) err := os.RemoveAll(newVersionPath) if err != nil { return fmt.Errorf("error while deleting directory: %w", err) } err = os.MkdirAll(newVersionPath, 0750) if err != nil { return fmt.Errorf("error while creating directory: %w", err) } // now I need to download artifacts buf := &bytes.Buffer{} urlpath, err := url.JoinPath(conf.RestartArtifactURL, newVersion, artifactName) if err != nil { return fmt.Errorf("error while joining artifact path %w", err) } err = bpfprogs.DownloadArtifact(urlpath, conf.HttpClientTimeout, buf) if err != nil { return fmt.Errorf("unable to download artifacts %w", err) } err = bpfprogs.ExtractArtifact(artifactName, buf, newVersionPath) if err != nil { return fmt.Errorf("unable to extract artifacts %w", err) } // removing symlinks for old version err = RemoveSymlink(filepath.Join(conf.BasePath, "latest/l3afd")) if err != nil { return fmt.Errorf("unable to remove symlink %w", err) } err = RemoveSymlink(filepath.Join(conf.BasePath, "latest/l3afd.cfg")) if err != nil { return fmt.Errorf("unable to remove symlink %w", err) } // adding new version symlinks err = AddSymlink(filepath.Join(newVersionPath, "l3afd", "l3afd"), filepath.Join(conf.BasePath, "latest/l3afd")) if err != nil { return fmt.Errorf("unable to add symlink %w", err) } err = AddSymlink(filepath.Join(newVersionPath, "l3afd", "l3afd.cfg"), filepath.Join(conf.BasePath, "latest/l3afd.cfg")) if err != nil { return fmt.Errorf("unable to add symlink %w", err) } return nil } // RollBackSymlink will rollback binary & cfgfile symlinks to old version func RollBackSymlink(oldCfgPath, oldBinPath string, oldVersion, newVersion string, conf *config.Config) error { if oldVersion == newVersion { return nil } err := RemoveSymlink(filepath.Join(conf.BasePath, "latest/l3afd")) if err != nil { return fmt.Errorf("unable to remove symlink %w", err) } err = RemoveSymlink(filepath.Join(conf.BasePath, "latest/l3afd.cfg")) if err != nil { return fmt.Errorf("unable to remove symlink %w", err) } // add new symlink err = AddSymlink(oldBinPath, filepath.Join(conf.BasePath, "latest/l3afd")) if err != nil { return fmt.Errorf("unable to add symlink %w", err) } err = AddSymlink(oldCfgPath, filepath.Join(conf.BasePath, "latest/l3afd.cfg")) if err != nil { return fmt.Errorf("unable to add symlink %w", err) } newVersionPath := filepath.Join(conf.BasePath, newVersion) if strings.Contains(newVersionPath, "..") { return fmt.Errorf("malicious path") } err = os.RemoveAll(newVersionPath) if err != nil { return fmt.Errorf("error while deleting directory: %w", err) } return nil } ================================================ FILE: restart/restart_test.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package restart import ( "testing" "github.com/l3af-project/l3afd/v2/models" ) func TestGetValueofLabel(t *testing.T) { b := []models.Label{ { Name: "iface", Value: "fakeif0", }, } q := "iface" ans := "fakeif0" t.Run("goodtest", func(t *testing.T) { if ans != getValueofLabel(q, b) { t.Errorf("GetValueofLabel failed") } }) } ================================================ FILE: routes/route.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package routes import "net/http" // Route defines a valid endpoint with the type of action supported on it type Route struct { Method string Path string HandlerFunc http.HandlerFunc } ================================================ FILE: routes/router.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package routes import ( chi "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" ) // NewRouter returns a router handle loaded with all the supported routes func NewRouter(routes []Route) *chi.Mux { r := chi.NewRouter() for _, route := range routes { r.Method(route.Method, route.Path, route.HandlerFunc) log.Info().Msgf("Route added:%+v\n", route) } return r } ================================================ FILE: signals/signal_unix.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build !WINDOWS // +build !WINDOWS package signals import ( "os" "syscall" ) var ShutdownSignals = []os.Signal{os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} ================================================ FILE: signals/signal_windows.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // //go:build WINDOWS // +build WINDOWS package signals import ( "os" ) var ShutdownSignals = []os.Signal{os.Interrupt} ================================================ FILE: stats/metrics.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 package stats import ( "errors" "net" "net/http" "github.com/l3af-project/l3afd/v2/models" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog/log" ) var ( BPFStartCount *prometheus.CounterVec BPFStopCount *prometheus.CounterVec BPFUpdateCount *prometheus.CounterVec BPFUpdateFailedCount *prometheus.CounterVec BPFRunning *prometheus.GaugeVec BPFStartTime *prometheus.GaugeVec BPFMonitorMap *prometheus.GaugeVec BPFDeployFailedCount *prometheus.CounterVec ) func SetupMetrics(hostname, daemonName, metricsAddr string) { bpfStartCountVec := promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: daemonName, Name: "BPFStartCount", Help: "The count of BPF program started", }, []string{"host", "ebpf_program", "direction", "interface_name", "ipv4_address"}, ) BPFStartCount = bpfStartCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfStopCountVec := promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: daemonName, Name: "BPFStopCount", Help: "The count of BPF program stopped", }, []string{"host", "ebpf_program", "direction", "interface_name", "ipv4_address"}, ) BPFStopCount = bpfStopCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfUpdateCountVec := promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: daemonName, Name: "BPFUpdateCount", Help: "The count of BPF programs updated", }, []string{"host", "ebpf_program", "direction", "interface_name", "ipv4_address"}, ) BPFUpdateCount = bpfUpdateCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfUpdateFailedCountVec := promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: daemonName, Name: "BPFUpdateFailedCount", Help: "The count of Failed BPF program update args", }, []string{"host", "bpf_program", "direction", "interface_name", "ipv4_address"}, ) BPFUpdateFailedCount = bpfUpdateFailedCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfRunningVec := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: daemonName, Name: "BPFRunning", Help: "This value indicates BPF program is running or not", }, []string{"host", "ebpf_program", "version", "direction", "interface_name", "ipv4_address"}, ) if err := prometheus.Register(bpfRunningVec); err != nil { log.Warn().Err(err).Msg("Failed to register BPFRunning metrics") } BPFRunning = bpfRunningVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfStartTimeVec := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: daemonName, Name: "BPFStartTime", Help: "This value indicates start time of the BPF program since unix epoch in seconds", }, []string{"host", "ebpf_program", "direction", "interface_name", "ipv4_address"}, ) if err := prometheus.Register(bpfStartTimeVec); err != nil { log.Warn().Err(err).Msg("Failed to register BPFStartTime metrics") } BPFStartTime = bpfStartTimeVec.MustCurryWith(prometheus.Labels{"host": hostname}) bpfMonitorMapVec := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: daemonName, Name: "BPFMonitorMap", Help: "This value indicates BPF program monitor counters", }, []string{"host", "ebpf_program", "map_name", "interface_name", "ipv4_address"}, ) if err := prometheus.Register(bpfMonitorMapVec); err != nil { log.Warn().Err(err).Msg("Failed to register BPFMonitorMap metrics") } BPFMonitorMap = bpfMonitorMapVec.MustCurryWith(prometheus.Labels{"host": hostname}) BPFDeployFailedCountVec := promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: daemonName, Name: "BPFDeployFailedCount", Help: "The count of BPF program failed to start or update", }, []string{"host", "ebpf_program", "direction", "interface_name", "ipv4_address"}, ) BPFDeployFailedCount = BPFDeployFailedCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) BPFStartCount = bpfStartCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) // Prometheus handler metricsHandler := promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}) // Adding web endpoint go func() { // Expose the registered metrics via HTTP. if _, ok := models.AllNetListeners.Load("stat_http"); !ok { tcpAddr, err := net.ResolveTCPAddr("tcp", metricsAddr) if err != nil { log.Fatal().Err(err).Msg("Error resolving TCP address") return } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { log.Fatal().Err(err).Msgf("unable to create net Listen") } models.AllNetListeners.Store("stat_http", listener) } http.Handle("/metrics", metricsHandler) val, _ := models.AllNetListeners.Load("stat_http") l, _ := val.(*net.TCPListener) if err := http.Serve(l, nil); !errors.Is(err, http.ErrServerClosed) { log.Fatal().Err(err).Msgf("Failed to launch prometheus metrics endpoint") } }() } func Add(value float64, counterVec *prometheus.CounterVec, ebpfProgram, direction, ifaceName string, ipv4Address string) { if counterVec == nil { log.Warn().Msg("Metrics: counter vector is nil and needs to be initialized before Incr") return } bpfCounter, err := counterVec.GetMetricWith( prometheus.Labels(map[string]string{ "ebpf_program": ebpfProgram, "direction": direction, "interface_name": ifaceName, "ipv4_address": ipv4Address, }), ) if err != nil { log.Warn().Msgf("Metrics: unable to fetch counter with fields: ebpf_program: %s, direction: %s, interface_name: %s", ebpfProgram, direction, ifaceName) return } bpfCounter.Add(value) } func Set(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, direction, ifaceName string, ipv4Address string) { if gaugeVec == nil { log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before Set") return } bpfGauge, err := gaugeVec.GetMetricWith( prometheus.Labels(map[string]string{ "ebpf_program": ebpfProgram, "direction": direction, "interface_name": ifaceName, "ipv4_address": ipv4Address, }), ) if err != nil { log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, direction: %s, interface_name: %s", ebpfProgram, direction, ifaceName) return } bpfGauge.Set(value) } // Set gaugevec metrics value with given mapName and other fields func SetValue(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, mapName, ifaceName string, ipv4Address string) { if gaugeVec == nil { log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before SetValue") return } bpfGauge, err := gaugeVec.GetMetricWith( prometheus.Labels(map[string]string{ "ebpf_program": ebpfProgram, "map_name": mapName, "interface_name": ifaceName, "ipv4_address": ipv4Address, }), ) if err != nil { log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, map_name: %s, interface_name: %s", ebpfProgram, mapName, ifaceName) return } bpfGauge.Set(value) } func SetWithVersion(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, version, direction, ifaceName string, ipv4Address string) { if gaugeVec == nil { log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before Set") return } bpfGauge, err := gaugeVec.GetMetricWith( prometheus.Labels(map[string]string{ "ebpf_program": ebpfProgram, "version": version, "direction": direction, "interface_name": ifaceName, "ipv4_address": ipv4Address, }), ) if err != nil { log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, version: %s, direction: %s, interface_name: %s", ebpfProgram, version, direction, ifaceName) return } bpfGauge.Set(value) } ================================================ FILE: testdata/Test_l3af-config.json ================================================ null ================================================ FILE: utils/utils.go ================================================ // Copyright Contributors to the L3AF Project. // SPDX-License-Identifier: Apache-2.0 // Package utils provides helper functions for l3afd package utils import ( "fmt" "os" "path/filepath" "strconv" "strings" ) // GetKernelVersion - reads the kernel version from /proc/version func GetKernelVersion() (string, error) { osVersion, err := os.ReadFile("/proc/version") if err != nil { return "", fmt.Errorf("failed to read procfs: %v", err) } var u1, u2, kernelVersion string _, err = fmt.Sscanf(string(osVersion), "%s %s %s", &u1, &u2, &kernelVersion) if err != nil { return "", fmt.Errorf("failed to scan procfs version: %v", err) } return kernelVersion, nil } // CheckTCXSupport - verifies kernel version is 6.8 or above func CheckTCXSupport() bool { const minVerLen = 2 const minMajorKernelVer = 6 const minMinorKernelVer = 8 kernelVersion, err := GetKernelVersion() if err != nil { return false } // validate version ver := strings.Split(kernelVersion, ".") if len(ver) < minVerLen { return false } majorVer, err := strconv.Atoi(ver[0]) if err != nil { return false } minorVer, err := strconv.Atoi(ver[1]) if err != nil { return false } if majorVer > minMajorKernelVer { return true } if majorVer == minMajorKernelVer && minorVer >= minMinorKernelVer { return true } return false } // ReplaceDotsWithUnderscores replaces all dots in the given string with underscores. func ReplaceDotsWithUnderscores(version string) string { return strings.ReplaceAll(version, ".", "_") } // LinkPinPath builds the formatted link pin path string. func LinkPinPath(bpfMapDefaultPath, ifaceName, programName, version, progType string) string { return filepath.Join(bpfMapDefaultPath, "links", ifaceName, programName, version, fmt.Sprintf("%s_%s", programName, progType)) } // TCLinkPinPath builds the formatted link pin path string. func TCLinkPinPath(bpfMapDefaultPath, ifaceName, programName, version, progType, direction string) string { return filepath.Join(bpfMapDefaultPath, "links", ifaceName, programName, version, fmt.Sprintf("%s_%s_%s", programName, progType, direction)) } // ProgPinPath builds the formatted program pin path string. func ProgPinPath(bpfMapDefaultPath, ifaceName, programName, version, entryFunctionName, progType string) string { return filepath.Join(bpfMapDefaultPath, "progs", ifaceName, programName, version, fmt.Sprintf("%s_%s", entryFunctionName, progType)) } ================================================ FILE: version.go ================================================ package main import ( "flag" "fmt" "os" "runtime" "time" ) var ( // Version binary version number Version string = "2.1.0" // SuffixTag binary tag (e.g. beta, dev, devel, production). SuffixTag string // VersionDate build timestamp (e.g. ). VersionDate string // VersionSHA git commit SHA. VersionSHA string versionFlag = flag.Bool("version", false, "display version information") ) // Init prints version information and exits, if --version option passed. func initVersion() { if *versionFlag { fmt.Println(VersionInfo()) os.Exit(0) } } // VersionInfo returns a formatted string of version data used in the version flag. func VersionInfo() string { var sha, buildDate string if btime, err := time.Parse("20060102150405", VersionDate); err == nil { buildDate = "built " + btime.Format(time.RFC3339) } else { currentFilePath, err := os.Executable() if err == nil { info, err := os.Stat(currentFilePath) if err == nil { buildDate = info.ModTime().Format(time.RFC3339) } } } if VersionSHA != "" { sha = "\nBuild SHA: " + VersionSHA } return fmt.Sprintf("Version: %s\nGo Version: %s\nBuild Date: %s%s", ShortVersion(), runtime.Version(), buildDate, sha) } // ShortVersion returns a formatted string containing only the Version and VersionTag func ShortVersion() string { if Version == "0.0.0" { SuffixTag = "dev" } if SuffixTag != "" { return fmt.Sprintf("%s-%s", Version, SuffixTag) } return Version }