├── .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.yml ├── config ├── config.go ├── config_loader.go └── l3afd.cfg ├── 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 /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the format of this file, see 5 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 6 | 7 | version: 2 8 | updates: 9 | 10 | - package-ecosystem: "github-actions" 11 | # Workflow files are stored in the default location of `.github/workflows` 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | day: "saturday" 16 | 17 | - package-ecosystem: "gomod" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | day: "saturday" 22 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | --- 3 | changelog: 4 | exclude: 5 | labels: 6 | - ignore-for-release 7 | categories: 8 | - title: Breaking Changes 💥 9 | labels: 10 | - breaking-change 11 | - breaking 12 | - title: New Features 🎉 13 | labels: 14 | - feat 15 | - enhancement 16 | - title: Bug Fixes 🐛 17 | labels: 18 | - fix 19 | - bugfix 20 | - bug 21 | - title: Other Changes 22 | labels: 23 | - "*" 24 | -------------------------------------------------------------------------------- /.github/workflows/ci-build-windows.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the github environment, see 5 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 6 | # 7 | # For documentation on the syntax of this file, see 8 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 9 | name: CI Windows build 10 | on: 11 | pull_request: {} 12 | push: 13 | branches: 14 | - main 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: windows-latest 22 | 23 | steps: 24 | - name: Setup Go 1.24.0 25 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 26 | with: 27 | go-version: '1.24.0' 28 | 29 | - name: Harden Runner 30 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 31 | with: 32 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 33 | 34 | - name: Set up git env 35 | run: | 36 | git config --global core.autocrlf false 37 | $gopath = (go env GOPATH) 38 | echo "GOPATH=$gopath" >> $env:GITHUB_ENV 39 | 40 | - name: Checkout repository 41 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 42 | 43 | - name: Format 44 | run: | 45 | go install golang.org/x/tools/cmd/goimports@latest 46 | $goimp = (Join-path -Path (go env GOPATH) -ChildPath "\bin\goimports") 47 | $res = (&$goimp -l .) -replace "$_" 48 | if ($res -ne "") { 49 | echo "Unformatted source code:" 50 | echo $res 51 | exit 1 52 | } 53 | 54 | - name: Vet 55 | run: | 56 | go vet -tags WINDOWS ./... 57 | 58 | - name: Test 59 | run: | 60 | go test -tags WINDOWS ./... 61 | 62 | - uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 63 | with: 64 | version: "2025.1.1" 65 | install-go: false 66 | cache-key: "1.24.x" 67 | build-tags: WINDOWS 68 | 69 | - name: Build 70 | env: 71 | GOPATH: ${{env.GOPATH}} 72 | run: | 73 | cmake -B build 74 | cmake --build build 75 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the github environment, see 5 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 6 | # 7 | # For documentation on the syntax of this file, see 8 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 9 | 10 | name: CI Ubuntu build 11 | on: 12 | pull_request: {} 13 | push: 14 | branches: 15 | - main 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | strategy: 23 | matrix: 24 | os: 25 | - ubuntu-24.04 26 | - ubuntu-22.04 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Setup Go 1.24.0 30 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c 31 | with: 32 | go-version: '1.24.0' 33 | 34 | - name: Harden Runner 35 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 36 | with: 37 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 38 | 39 | - name: Set up environment 40 | run: | 41 | sudo apt-get update 42 | sudo apt-get remove -y containerd.io docker docker.io moby-engine moby-cli || true # Remove any existing Docker-related packages 43 | sudo apt-get install -y \ 44 | apt-transport-https \ 45 | ca-certificates \ 46 | curl \ 47 | software-properties-common 48 | # docker src 49 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 50 | 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 51 | sudo apt-get update 52 | sudo apt-get install -y docker-ce docker-ce-cli containerd.io 53 | sudo apt-get install -y gcc libc-dev bash perl curl make 54 | 55 | - name: Checkout repository 56 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 57 | 58 | - name: Format 59 | run: | 60 | go install golang.org/x/tools/cmd/goimports@latest 61 | res="$(goimports -l .)" 62 | if [[ "$(printf '%s' "$res")" != '' ]]; then 63 | echo "Unformatted source code:" 64 | echo "$res" 65 | exit 1 66 | fi 67 | 68 | - name: Vet 69 | run: | 70 | go vet ./... 71 | 72 | - name: Test 73 | run: | 74 | go test ./... 75 | go clean -modcache 76 | 77 | - uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 78 | with: 79 | version: "2025.1.1" 80 | install-go: false 81 | cache-key: "1.24.x" 82 | 83 | - name: Build 84 | run: | 85 | make 86 | 87 | - name: Copy files 88 | if: github.ref == 'refs/heads/main' 89 | run: | 90 | sudo cp ./config/l3afd.cfg ./build-docker 91 | sudo cp l3afd ./build-docker 92 | 93 | - name: login to docker registry 94 | if: github.ref == 'refs/heads/main' 95 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 96 | with: 97 | username: ${{secrets.DOCKER_USERNAME}} 98 | password: ${{secrets.DOCKER_TOKEN}} 99 | 100 | - name: build and push docker image to registry 101 | if: github.ref == 'refs/heads/main' 102 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 103 | with: 104 | context: ./build-docker 105 | push: true 106 | tags: linuxfoundationl3af/l3afd:latest 107 | 108 | - name: upload l3afd binary 109 | if: github.ref == 'refs/heads/main' 110 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 111 | with: 112 | name: l3afd-latest-linux-x86_64-${{ matrix.os }} 113 | path: l3afd 114 | 115 | -------------------------------------------------------------------------------- /.github/workflows/ci-e2e.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the github environment, see 5 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 6 | # 7 | # For documentation on the syntax of this file, see 8 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 9 | 10 | name: CI E2E build 11 | on: 12 | pull_request: {} 13 | push: 14 | branches: 15 | - main 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | strategy: 23 | matrix: 24 | os: 25 | - ubuntu-24.04 26 | - ubuntu-22.04 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Update and firewall stop 30 | run: | 31 | sudo apt update 32 | sudo systemctl stop ufw 33 | sudo apt install -y iproute2 34 | sudo apt install git curl hey 35 | - name: Checkout repository 36 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 37 | 38 | - name: Prep 39 | run: | 40 | sudo cp -r /home/runner/work/l3afd/l3afd /root 41 | sudo git clone https://github.com/l3af-project/l3af-arch.git /root/l3af-arch 42 | sudo bash /root/l3af-arch/dev_environment/e2e_test/prep_env.sh 43 | sudo bash /root/l3af-arch/dev_environment/setup_linux_dev_env.sh --ci-build 44 | hm=$(hostname) 45 | sudo find /root/l3af-arch/dev_environment/e2e_test -type f -name "*.json" -exec sed -i "s/l3af-test-host/$hm/g" {} + 46 | 47 | - name: Run Tests 48 | run: | 49 | sudo bash /root/l3af-arch/dev_environment/e2e_test/test_suite.sh 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the github environment, see 5 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 6 | # 7 | # For documentation on the syntax of this file, see 8 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 9 | 10 | name: "CodeQL" 11 | 12 | on: 13 | push: 14 | branches: [ main ] 15 | pull_request: 16 | branches: [ main ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'go' ] 34 | 35 | steps: 36 | - name: Harden Runner 37 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 38 | with: 39 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 40 | 41 | - name: Checkout repository 42 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 43 | 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b 46 | with: 47 | languages: ${{ matrix.language }} 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b 51 | -------------------------------------------------------------------------------- /.github/workflows/scorecards-analysis.yml: -------------------------------------------------------------------------------- 1 | # Copyright Contributors to the L3AF Project. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | # For documentation on the github environment, see 5 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners 6 | # 7 | # For documentation on the syntax of this file, see 8 | # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 9 | 10 | name: Scorecards 11 | 12 | on: 13 | push: 14 | branches: [ main ] 15 | pull_request: 16 | branches: [ main ] 17 | 18 | concurrency: 19 | # Cancel any Scorecards workflow currently in progress for the same PR. 20 | # Allow running concurrently with any other commits. 21 | group: scorecards-${{ github.event.pull_request.number || github.sha }} 22 | cancel-in-progress: true 23 | 24 | # Declare default permissions as read only. 25 | permissions: read-all 26 | 27 | jobs: 28 | analysis: 29 | name: Scorecards analysis 30 | runs-on: ubuntu-latest 31 | permissions: 32 | # Needed to upload the results to code-scanning dashboard. 33 | security-events: write 34 | id-token: write 35 | actions: read 36 | contents: read 37 | 38 | steps: 39 | - name: "Checkout code" 40 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 41 | with: 42 | persist-credentials: false 43 | 44 | - name: "Run analysis" 45 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 46 | with: 47 | results_file: results.sarif 48 | results_format: sarif 49 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 50 | # - you want to enable the Branch-Protection check on a *public* repository, or 51 | # - you are installing Scorecard on a *private* repository 52 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 53 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 54 | 55 | # Public repositories: 56 | # - Publish results to OpenSSF REST API for easy access by consumers 57 | # - Allows the repository to include the Scorecard badge. 58 | # - See https://github.com/ossf/scorecard-action#publishing-results. 59 | # For private repositories: 60 | # - `publish_results` will always be set to `false`, regardless 61 | # of the value entered here. 62 | publish_results: ${{ github.event_name != 'pull_request' }} 63 | 64 | 65 | # Upload the results as artifacts (optional). 66 | - name: "Upload artifact" 67 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 68 | with: 69 | name: SARIF file 70 | path: results.sarif 71 | retention-days: 5 72 | 73 | # Upload the results to GitHub's code scanning dashboard so it will be visible 74 | # at https://github.com/l3af-project/l3afd/security/code-scanning. 75 | - name: "Upload to code-scanning" 76 | uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b 77 | with: 78 | sarif_file: results.sarif 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(l3afd) 3 | 4 | add_custom_target(swagger ALL 5 | DEPENDS ${CMAKE_SOURCE_DIR}/docs/docs.go 6 | ${CMAKE_SOURCE_DIR}/docs/swagger.json 7 | ${CMAKE_SOURCE_DIR}/docs/swagger.yaml) 8 | 9 | add_custom_command(OUTPUT $ENV{GOPATH}/bin/swag.exe 10 | COMMAND go install github.com/swaggo/swag/cmd/swag@latest 11 | COMMAND go get -u github.com/swaggo/http-swagger 12 | COMMAND go get -u github.com/alecthomas/template) 13 | 14 | add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/docs/docs.go 15 | ${CMAKE_SOURCE_DIR}/docs/swagger.json 16 | ${CMAKE_SOURCE_DIR}/docs/swagger.yaml 17 | DEPENDS ${CMAKE_SOURCE_DIR}/apis/configwatch.go 18 | $ENV{GOPATH}/bin/swag.exe 19 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 20 | COMMAND "$ENV{GOPATH}/bin/swag.exe" init -d "./" -g "apis/configwatch.go") 21 | 22 | add_custom_target(build ALL 23 | DEPENDS ${CMAKE_SOURCE_DIR}/l3afd.exe) 24 | 25 | if (${WIN32}) 26 | add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/l3afd.exe 27 | DEPENDS ${CMAKE_SOURCE_DIR}/main.go 28 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 29 | COMMAND go build -tags WINDOWS .) 30 | else () 31 | add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/l3afd.exe 32 | DEPENDS ${CMAKE_SOURCE_DIR}/main.go 33 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 34 | COMMAND go build .) 35 | endif () 36 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default Code Owners 2 | 3 | * @sanfern @charleskbliu0 @jniesz @dalalkaran @pmoroney 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | export GOPATH := $(HOME)/go 4 | all: swagger build 5 | 6 | swagger: 7 | @mkdir $(GOPATH) || true 8 | @go install github.com/swaggo/swag/cmd/swag@latest 9 | @$(GOPATH)/bin/swag init -d "./" -g "apis/configwatch.go" 10 | 11 | build: 12 | @CGO_ENABLED=0 go build -ldflags \ 13 | "-X main.Version=v2.1.0 \ 14 | -X main.VersionSHA=`git rev-parse HEAD`" 15 | install: swagger 16 | @go mod tidy 17 | @CGO_ENABLED=0 go install -ldflags \ 18 | "-X main.Version=v2.1.0 \ 19 | -X main.VersionSHA=`git rev-parse HEAD`" 20 | cibuild: swagger 21 | @go mod tidy 22 | @CGO_ENABLED=0 go install -cover -ldflags \ 23 | "-X main.Version=v2.1.0 \ 24 | -X main.VersionSHA=`git rev-parse HEAD`" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # L3AFD: Lightweight eBPF Daemon 2 | ![L3AF_Logo](https://github.com/l3af-project/l3af-arch/blob/main/images/logos/Color/L3AF_logo.svg) 3 | 4 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6075/badge)](https://bestpractices.coreinfrastructure.org/projects/6075) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/l3af-project/l3afd)](https://goreportcard.com/report/github.com/l3af-project/l3afd) 6 | [![GoDoc](https://godoc.org/github.com/l3af-project/l3afd?status.svg)](https://pkg.go.dev/github.com/l3af-project/l3afd) 7 | [![Apache licensed](https://img.shields.io/badge/license-Apache-blue.svg)](LICENSE) 8 | [![L3AF Slack](https://img.shields.io/badge/slack-L3AF-brightgreen.svg?logo=slack)](http://l3afworkspace.slack.com/) 9 | 10 | L3AFD is a crucial part of the L3AF ecosystem. For more information on L3AF see 11 | https://l3af.io/ 12 | 13 | # Overview 14 | L3AFD is the primary component of the L3AF control plane. L3AFD is a daemon 15 | that orchestrates and manages multiple eBPF programs. L3AFD runs on each node 16 | where the user wishes to run eBPF programs. L3AFD reads configuration data and 17 | manages the execution and monitoring of eBPF programs running on the node. 18 | 19 | L3AFD downloads pre-built eBPF programs from a user-configured repository. 20 | However, we envision the creation of a community-driven eBPF package marketplace 21 | where L3AF users can obtain a variety of eBPF programs developed by multiple 22 | sources. 23 | 24 | ![L3AF Platform](https://github.com/l3af-project/l3af-arch/blob/main/images/L3AF_platform.png) 25 | 26 | # Try it out 27 | See our [L3AF Development Environment](https://github.com/l3af-project/l3af-arch/tree/main/dev_environment) 28 | for a quick and easy way to try out L3AF on your local machine. 29 | 30 | # Installing 31 | Try [a binary release](https://github.com/l3af-project/l3afd/releases/latest). 32 | 33 | # Building 34 | To build on your local machine, including swagger docs do the following. 35 | 36 | For Linux: 37 | ``` 38 | make 39 | ``` 40 | 41 | For Windows: 42 | ``` 43 | cmake -B build 44 | cmake --build build 45 | ``` 46 | # Docker build 47 | - L3AFD binary & configuration that is required in the Docker image needs to be built locally and copied to build-docker directory 48 | - Execute below command to build the docker image 49 | ``` 50 | docker build -t l3afd: -f Dockerfile . 51 | ``` 52 | Requirements to run L3AFD as a Container 53 | - BPF, debugfs & shared-memory filesystems mount points should be available in the container 54 | - L3AFD container needs privileged access as it needs to manage eBPF programs 55 | - eBPF programs should be attached to the host interface so that it will apply to all the containers in the host 56 | 57 | In order to satisfy the above requirements L3afd docker container needs to be run using the below command 58 | ``` 59 | 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: 60 | ``` 61 | # Testing 62 | To test on your local machine, do the following. 63 | 64 | For Linux: 65 | ``` 66 | go test ./... 67 | ``` 68 | 69 | For Windows: 70 | ``` 71 | go test -tags WINDOWS ./... 72 | ``` 73 | 74 | # Generate Swagger Docs 75 | See our [Swaggo setup](docs/swagger.md) 76 | 77 | # Contributing 78 | Contributing to L3afd is fun. To get started: 79 | - [Contributing guide](docs/CONTRIBUTING.md) 80 | -------------------------------------------------------------------------------- /apis/configwatch.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !configs 5 | // +build !configs 6 | 7 | package apis 8 | 9 | import ( 10 | "context" 11 | "crypto/tls" 12 | "crypto/x509" 13 | "encoding/pem" 14 | "errors" 15 | "fmt" 16 | "net" 17 | "net/http" 18 | "os" 19 | "os/signal" 20 | "path" 21 | "regexp" 22 | "strings" 23 | "time" 24 | "unicode/utf8" 25 | 26 | httpSwagger "github.com/swaggo/http-swagger" 27 | 28 | "github.com/l3af-project/l3afd/v2/bpfprogs" 29 | "github.com/l3af-project/l3afd/v2/config" 30 | "github.com/l3af-project/l3afd/v2/models" 31 | "github.com/l3af-project/l3afd/v2/routes" 32 | "github.com/l3af-project/l3afd/v2/signals" 33 | 34 | _ "github.com/l3af-project/l3afd/v2/docs" 35 | 36 | "github.com/rs/zerolog/log" 37 | ) 38 | 39 | type Server struct { 40 | BPFRTConfigs *bpfprogs.NFConfigs 41 | HostName string 42 | l3afdServer *http.Server 43 | CaCertPool *x509.CertPool 44 | SANMatchRules []string 45 | } 46 | 47 | // @title L3AFD APIs 48 | // @version 1.0 49 | // @description Configuration APIs to deploy and get the details of the eBPF Programs on the node 50 | // @host 51 | // @BasePath / 52 | func StartConfigWatcher(ctx context.Context, hostname, daemonName string, conf *config.Config, bpfrtconfg *bpfprogs.NFConfigs) error { 53 | log.Info().Msgf("%s config server setup started on host %s", daemonName, hostname) 54 | 55 | s := &Server{ 56 | BPFRTConfigs: bpfrtconfg, 57 | HostName: hostname, 58 | l3afdServer: &http.Server{ 59 | Addr: conf.L3afConfigsRestAPIAddr, 60 | }, 61 | SANMatchRules: conf.MTLSSANMatchRules, 62 | } 63 | if _, ok := models.AllNetListeners.Load("main_http"); !ok { 64 | tcpAddr, err := net.ResolveTCPAddr("tcp", conf.L3afConfigsRestAPIAddr) 65 | if err != nil { 66 | return fmt.Errorf("error resolving TCP address:%w", err) 67 | } 68 | listener, err := net.ListenTCP("tcp", tcpAddr) 69 | if err != nil { 70 | return fmt.Errorf("creating tcp listner failed with %w", err) 71 | } 72 | models.AllNetListeners.Store("main_http", listener) 73 | } 74 | term := make(chan os.Signal, 1) 75 | signal.Notify(term, signals.ShutdownSignals...) 76 | go func() { 77 | <-term 78 | s.GracefulStop(conf.ShutdownTimeout) 79 | ctx.Done() 80 | log.Info().Msg("L3afd gracefulStop completed") 81 | }() 82 | 83 | go func() { 84 | r := routes.NewRouter(apiRoutes(ctx, bpfrtconfg)) 85 | if conf.SwaggerApiEnabled { 86 | r.Mount("/swagger", httpSwagger.WrapHandler) 87 | } 88 | s.l3afdServer.Handler = r 89 | 90 | // As per design discussion when mTLS flag is not set and not listening on loopback or localhost 91 | if !conf.MTLSEnabled && !isLoopback(conf.L3afConfigsRestAPIAddr) && conf.Environment == config.ENV_PROD { 92 | conf.MTLSEnabled = true 93 | } 94 | val, _ := models.AllNetListeners.Load("main_http") 95 | l, _ := val.(*net.TCPListener) 96 | if conf.MTLSEnabled { 97 | log.Info().Msgf("l3afd server listening with mTLS - %s ", conf.L3afConfigsRestAPIAddr) 98 | // Create a CA certificate pool and add client ca's to it 99 | caCert, err := os.ReadFile(path.Join(conf.MTLSCertDir, conf.MTLSCACertFilename)) 100 | if err != nil { 101 | log.Fatal().Err(err).Msgf("client CA %s file not found", conf.MTLSCACertFilename) 102 | } 103 | 104 | s.CaCertPool, _ = x509.SystemCertPool() 105 | if s.CaCertPool == nil { 106 | s.CaCertPool = x509.NewCertPool() 107 | } 108 | if ok := s.CaCertPool.AppendCertsFromPEM(caCert); !ok { 109 | log.Warn().Msgf("No client certs appended for mTLS") 110 | } 111 | serverCertFile := path.Join(conf.MTLSCertDir, conf.MTLSServerCertFilename) 112 | serverKeyFile := path.Join(conf.MTLSCertDir, conf.MTLSServerKeyFilename) 113 | serverCert, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile) 114 | if err != nil { 115 | log.Fatal().Err(err).Msgf("failure loading certs") 116 | } 117 | // build server config 118 | s.l3afdServer.TLSConfig = &tls.Config{ 119 | Certificates: []tls.Certificate{serverCert}, 120 | GetConfigForClient: func(hi *tls.ClientHelloInfo) (*tls.Config, error) { 121 | serverConf := &tls.Config{ 122 | Certificates: []tls.Certificate{serverCert}, 123 | MinVersion: tls.VersionTLS12, 124 | ClientAuth: tls.RequireAndVerifyClientCert, 125 | ClientCAs: s.CaCertPool, 126 | VerifyPeerCertificate: s.getClientValidator(hi), 127 | } 128 | return serverConf, nil 129 | }, 130 | } 131 | 132 | cpb, _ := pem.Decode(caCert) 133 | cert, err := x509.ParseCertificate(cpb.Bytes) 134 | if err != nil { 135 | log.Fatal().Err(err).Msgf("error in parsing tls certificate : %v", conf.MTLSCACertFilename) 136 | } 137 | expiry := cert.NotAfter 138 | start := cert.NotBefore 139 | go func() { 140 | period := time.Hour * 24 141 | ticker := time.NewTicker(period) 142 | defer ticker.Stop() 143 | for { 144 | select { 145 | case <-ticker.C: 146 | MonitorTLS(start, expiry, conf) 147 | case <-ctx.Done(): 148 | return 149 | } 150 | } 151 | }() 152 | if err := s.l3afdServer.ServeTLS(l, serverCertFile, serverKeyFile); !errors.Is(err, http.ErrServerClosed) { 153 | log.Fatal().Err(err).Msgf("failed to start L3AFD server with mTLS enabled") 154 | } 155 | } else { 156 | log.Info().Msgf("l3afd server listening - %s ", conf.L3afConfigsRestAPIAddr) 157 | if err := s.l3afdServer.Serve(l); !errors.Is(err, http.ErrServerClosed) { 158 | log.Fatal().Err(err).Msgf("failed to start L3AFD server") 159 | } 160 | } 161 | }() 162 | return nil 163 | } 164 | 165 | func (s *Server) GracefulStop(shutdownTimeout time.Duration) error { 166 | log.Info().Msg("L3afd graceful stop initiated") 167 | exitCode := 0 168 | if len(s.BPFRTConfigs.IngressXDPBpfs) > 0 || len(s.BPFRTConfigs.IngressTCBpfs) > 0 || len(s.BPFRTConfigs.EgressTCBpfs) > 0 || s.BPFRTConfigs.ProbesBpfs.Len() > 0 { 169 | ctx, cancelfunc := context.WithTimeout(context.Background(), shutdownTimeout) 170 | defer cancelfunc() 171 | if err := s.BPFRTConfigs.Close(ctx); err != nil { 172 | log.Error().Err(err).Msg("stopping all network functions failed") 173 | exitCode = 1 174 | } 175 | } 176 | os.Exit(exitCode) 177 | return nil 178 | } 179 | 180 | // isLoopback - Check for localhost or loopback address 181 | func isLoopback(addr string) bool { 182 | 183 | if strings.Contains(addr, "localhost:") { 184 | return true 185 | } 186 | if id := strings.LastIndex(addr, ":"); id > -1 { 187 | addr = addr[:id] 188 | } 189 | if ipAddr := net.ParseIP(addr); ipAddr != nil { 190 | return ipAddr.IsLoopback() 191 | } 192 | // :port scenario 193 | return true 194 | } 195 | 196 | func MonitorTLS(start time.Time, expiry time.Time, conf *config.Config) { 197 | todayDate := time.Now() 198 | expiryDate := expiry 199 | startDate := start 200 | diff := expiryDate.Sub(todayDate) 201 | remainingHoursToStart := todayDate.Sub(startDate) 202 | limit := conf.MTLSCertExpiryWarningDays * 24 203 | remainingHoursToExpire := int(diff.Hours()) 204 | if remainingHoursToStart > 0 { 205 | log.Fatal().Msgf("tls certificate start from : %v", startDate) 206 | } 207 | if remainingHoursToExpire <= limit { 208 | if remainingHoursToExpire < 0 { 209 | log.Fatal().Msgf("tls certificate is expired on : %v", expiryDate) 210 | } else { 211 | log.Warn().Msgf("tls certificate will expire in %v days", int64(remainingHoursToExpire/24)) 212 | } 213 | } 214 | } 215 | 216 | func (s *Server) getClientValidator(helloInfo *tls.ClientHelloInfo) func([][]byte, [][]*x509.Certificate) error { 217 | 218 | log.Debug().Msgf("Inside get client validator - %v", helloInfo.Conn.RemoteAddr()) 219 | return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 220 | // Verifying client certs with root ca 221 | opts := x509.VerifyOptions{ 222 | Roots: s.CaCertPool, 223 | CurrentTime: time.Now(), 224 | Intermediates: x509.NewCertPool(), 225 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 226 | } 227 | _, err := verifiedChains[0][0].Verify(opts) 228 | if err != nil { 229 | log.Error().Err(err).Msgf("certs verification failed") 230 | return err 231 | } 232 | 233 | log.Debug().Msgf("validating with SAN match rules - %s", s.SANMatchRules) 234 | if len(s.SANMatchRules) == 0 { 235 | return nil 236 | } 237 | for _, dnsName := range verifiedChains[0][0].DNSNames { 238 | if !validHostname(dnsName, true) { 239 | continue 240 | } 241 | dnsName = toLowerCaseASCII(dnsName) 242 | for _, sanMatchRule := range s.SANMatchRules { 243 | sanMatchRule = toLowerCaseASCII(sanMatchRule) 244 | if matchExactly(dnsName, sanMatchRule) { 245 | log.Debug().Msgf("Successfully matched matchExactly cert dns %s SANMatchRule %s", dnsName, sanMatchRule) 246 | return nil 247 | } else if matchHostnamesWithRegexp(dnsName, sanMatchRule) { 248 | log.Debug().Msgf("Successfully matched matchHostnamesWithRegexp cert dns %s SANMatchRule %s", dnsName, sanMatchRule) 249 | return nil 250 | } 251 | } 252 | } 253 | 254 | err = errors.New("certs verification with SAN match not found") 255 | log.Error().Err(err).Msgf("SAN match rules %s", s.SANMatchRules) 256 | return err 257 | } 258 | } 259 | 260 | // toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use 261 | // an explicitly ASCII function to avoid any sharp corners resulting from 262 | // performing Unicode operations on DNS labels. 263 | func toLowerCaseASCII(in string) string { 264 | // If the string is already lower-case then there's nothing to do. 265 | isAlreadyLowerCase := true 266 | for _, c := range in { 267 | if c == utf8.RuneError { 268 | // If we get a UTF-8 error then there might be 269 | // upper-case ASCII bytes in the invalid sequence. 270 | isAlreadyLowerCase = false 271 | break 272 | } 273 | if 'A' <= c && c <= 'Z' { 274 | isAlreadyLowerCase = false 275 | break 276 | } 277 | } 278 | 279 | if isAlreadyLowerCase { 280 | return in 281 | } 282 | 283 | out := []byte(in) 284 | for i, c := range out { 285 | if 'A' <= c && c <= 'Z' { 286 | out[i] += 'a' - 'A' 287 | } 288 | } 289 | return string(out) 290 | } 291 | 292 | // validHostname reports whether host is a valid hostname that can be matched or 293 | // matched against according to RFC 6125 2.2, with some leniency to accommodate 294 | // legacy values. 295 | func validHostname(host string, isPattern bool) bool { 296 | if !isPattern { 297 | host = strings.TrimSuffix(host, ".") 298 | } 299 | if len(host) == 0 { 300 | return false 301 | } 302 | 303 | for i, part := range strings.Split(host, ".") { 304 | if part == "" { 305 | // Empty label. 306 | return false 307 | } 308 | if isPattern && i == 0 && part == "*" { 309 | // Only allow full left-most wildcards, as those are the only ones 310 | // we match, and matching literal '*' characters is probably never 311 | // the expected behavior. 312 | continue 313 | } 314 | for j, c := range part { 315 | if 'a' <= c && c <= 'z' { 316 | continue 317 | } 318 | if '0' <= c && c <= '9' { 319 | continue 320 | } 321 | if 'A' <= c && c <= 'Z' { 322 | continue 323 | } 324 | if c == '-' && j != 0 { 325 | continue 326 | } 327 | if c == '_' { 328 | // Not a valid character in hostnames, but commonly 329 | // found in deployments outside the WebPKI. 330 | continue 331 | } 332 | return false 333 | } 334 | } 335 | 336 | return true 337 | } 338 | 339 | // matchExactly - match hostnames 340 | func matchExactly(hostA, hostB string) bool { 341 | // Here checking hostB (i.e. sanMatchRule) is valid hostname and not regex/pattern 342 | if !validHostname(hostB, false) { 343 | return false 344 | } 345 | return hostA == hostB 346 | } 347 | 348 | // matchHostnamesWithRegexp - To match the san rules with regexp 349 | func matchHostnamesWithRegexp(dnsName, sanMatchRule string) bool { 350 | defer func() bool { 351 | if err := recover(); err != nil { 352 | log.Warn().Msgf("panic occurred: %v", err) 353 | } 354 | return false 355 | }() 356 | if len(dnsName) == 0 || len(sanMatchRule) == 0 { 357 | return false 358 | } 359 | re := regexp.MustCompile(sanMatchRule) 360 | 361 | return re.MatchString(dnsName) 362 | } 363 | -------------------------------------------------------------------------------- /apis/configwatch_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package apis 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestMatchHostnamesWithRegexp(t *testing.T) { 12 | type args struct { 13 | dnsName string 14 | sanMatchRule string 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want bool 20 | wantErr bool 21 | }{ 22 | { 23 | name: "EmptyCheck", 24 | args: args{dnsName: "", sanMatchRule: ""}, 25 | want: false, 26 | wantErr: false, 27 | }, 28 | { 29 | name: "LengthMissMatchCheck", 30 | args: args{dnsName: "l3afd-lfn.us.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, 31 | want: false, 32 | wantErr: false, 33 | }, 34 | { 35 | name: "LengthMatchCheck", 36 | args: args{dnsName: "l3afd-lfn.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, 37 | want: true, 38 | wantErr: false, 39 | }, 40 | { 41 | name: "LengthMatchPatternMissCheck", 42 | args: args{dnsName: "l3afd-us.l3af.io", sanMatchRule: "l3afd-lf.l3af.io"}, 43 | want: false, 44 | wantErr: false, 45 | }, 46 | { 47 | name: "PatternMatchCheck", 48 | args: args{dnsName: "l3afd-*.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, 49 | want: false, 50 | wantErr: false, 51 | }, 52 | { 53 | name: "PatternMissMatchCheck", 54 | args: args{dnsName: "*l3afd-.l3af.io", sanMatchRule: "l3afd-lfn.l3af.io"}, 55 | want: false, 56 | wantErr: true, 57 | }, 58 | { 59 | name: "PatternRegExMatchCheck", 60 | args: args{dnsName: "asnl3afd-lfn.l3af.io", sanMatchRule: ".*l3afd-lfn.l3af.io"}, 61 | want: true, 62 | wantErr: false, 63 | }, 64 | { 65 | name: "PatternRegExExactMatchCheck", 66 | args: args{dnsName: "l3afd-dev.l3af.io", sanMatchRule: "^dev.l3af.io$"}, 67 | want: false, 68 | wantErr: false, 69 | }, 70 | { 71 | name: "PatternRegExFindMatch", 72 | args: args{dnsName: "l3afd-dev.l3af.io", sanMatchRule: "dev.l3af.io"}, 73 | want: true, 74 | wantErr: false, 75 | }, 76 | { 77 | name: "PatternRegExFindMatchPattern", 78 | args: args{dnsName: "l3afd-dev-10.l3af.io", sanMatchRule: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, 79 | want: true, 80 | wantErr: false, 81 | }, 82 | { 83 | name: "PatternRegExLowerCaseMatch", 84 | args: args{dnsName: "l3afd-dev-a0.l3af.io", sanMatchRule: "^l3afd-dev-[a-z][0-9]\\.l3af\\.io$"}, 85 | want: true, 86 | wantErr: false, 87 | }, 88 | { 89 | name: "PatternRegExUpperCaseMatch", 90 | args: args{dnsName: "l3afd-dev-A0.l3af.io", sanMatchRule: "^l3afd-dev-[A-Z][0-9]\\.l3af\\.io$"}, 91 | want: true, 92 | wantErr: false, 93 | }, 94 | { 95 | name: "PatternRegExPanicCheck", 96 | args: args{dnsName: "l3afd-dev-A0.l3af.io", sanMatchRule: "*l3afd-dev.l3af.io"}, 97 | want: false, 98 | wantErr: true, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | got := matchHostnamesWithRegexp(tt.args.dnsName, tt.args.sanMatchRule) 104 | if !reflect.DeepEqual(got, tt.want) { 105 | t.Errorf("matchHostnamesWithRegexp() = %v, want %v", got, tt.want) 106 | } 107 | }) 108 | } 109 | } 110 | 111 | func TestMatchExactly(t *testing.T) { 112 | type args struct { 113 | hostA string 114 | hostB string 115 | } 116 | tests := []struct { 117 | name string 118 | args args 119 | want bool 120 | wantErr bool 121 | }{ 122 | { 123 | name: "EmptyCheck", 124 | args: args{hostA: "", hostB: ""}, 125 | want: false, 126 | wantErr: false, 127 | }, 128 | { 129 | name: "ExactMatchCheck", 130 | args: args{hostA: "l3afd-lfn.l3af.io", hostB: "l3afd-lfn.l3af.io"}, 131 | want: true, 132 | wantErr: false, 133 | }, 134 | } 135 | for _, tt := range tests { 136 | t.Run(tt.name, func(t *testing.T) { 137 | got := matchExactly(tt.args.hostA, tt.args.hostB) 138 | if !reflect.DeepEqual(got, tt.want) { 139 | t.Errorf("matchHostnames() = %v, want %v", got, tt.want) 140 | } 141 | }) 142 | } 143 | } 144 | 145 | func TestToLowerCaseASCII(t *testing.T) { 146 | type args struct { 147 | in string 148 | } 149 | tests := []struct { 150 | name string 151 | args args 152 | want string 153 | wantErr bool 154 | }{ 155 | { 156 | name: "EmptyCheck", 157 | args: args{in: ""}, 158 | want: "", 159 | wantErr: false, 160 | }, 161 | { 162 | name: "RegexLowerCheck", 163 | args: args{in: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, 164 | want: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$", 165 | wantErr: false, 166 | }, 167 | { 168 | name: "RegexUpperValueCheck", 169 | args: args{in: "^L3AFd-dev-[0-9][0-9]\\.l3af\\.io$"}, 170 | want: "^l3afd-dev-[0-9][0-9]\\.l3af\\.io$", 171 | wantErr: false, 172 | }, 173 | { 174 | name: "RegexLowerCheckRuneError", 175 | args: args{in: "^�l3afd-dev-[0-9][0-9]\\.l3af\\.io$"}, 176 | want: "^�l3afd-dev-[0-9][0-9]\\.l3af\\.io$", 177 | wantErr: false, 178 | }, 179 | } 180 | for _, tt := range tests { 181 | t.Run(tt.name, func(t *testing.T) { 182 | got := toLowerCaseASCII(tt.args.in) 183 | if !reflect.DeepEqual(got, tt.want) { 184 | t.Errorf("matchHostnames() = %v, want %v", got, tt.want) 185 | } 186 | }) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /apis/handlers/addprog.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | 12 | "net/http" 13 | 14 | "github.com/rs/zerolog/log" 15 | 16 | "github.com/l3af-project/l3afd/v2/bpfprogs" 17 | "github.com/l3af-project/l3afd/v2/models" 18 | ) 19 | 20 | // AddEbpfPrograms add new eBPF programs on node 21 | // @Summary Adds new eBPF Programs on node 22 | // @Description Adds new eBPF Programs on node 23 | // @Accept json 24 | // @Produce json 25 | // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" 26 | // @Success 200 27 | // @Router /l3af/configs/v1/add [post] 28 | func AddEbpfPrograms(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { 29 | 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | mesg := "" 32 | statusCode := http.StatusOK 33 | 34 | w.Header().Add("Content-Type", "application/json") 35 | 36 | defer func(mesg *string, statusCode *int) { 37 | w.WriteHeader(*statusCode) 38 | _, err := w.Write([]byte(*mesg)) 39 | if err != nil { 40 | log.Warn().Msgf("Failed to write response bytes: %v", err) 41 | } 42 | }(&mesg, &statusCode) 43 | if models.IsReadOnly { 44 | log.Warn().Msgf("We are in between restart please try after some time") 45 | mesg = "We are currently in the middle of a restart. Please attempt again after a while." 46 | return 47 | } 48 | defer DecWriteReq() 49 | IncWriteReq() 50 | if r.Body == nil { 51 | log.Warn().Msgf("Empty request body") 52 | return 53 | } 54 | bodyBuffer, err := io.ReadAll(r.Body) 55 | if err != nil { 56 | mesg = fmt.Sprintf("failed to read request body: %v", err) 57 | log.Error().Msg(mesg) 58 | statusCode = http.StatusInternalServerError 59 | return 60 | } 61 | 62 | var t []models.L3afBPFPrograms 63 | if err := json.Unmarshal(bodyBuffer, &t); err != nil { 64 | mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) 65 | log.Error().Msg(mesg) 66 | statusCode = http.StatusInternalServerError 67 | return 68 | } 69 | 70 | if err := bpfcfg.AddeBPFPrograms(t); err != nil { 71 | mesg = fmt.Sprintf("failed to AddEbpfPrograms : %v", err) 72 | log.Error().Msg(mesg) 73 | 74 | statusCode = http.StatusInternalServerError 75 | return 76 | } 77 | } 78 | } 79 | 80 | func IncWriteReq() { 81 | models.StateLock.Lock() 82 | models.CurrentWriteReq++ 83 | models.StateLock.Unlock() 84 | } 85 | func DecWriteReq() { 86 | models.StateLock.Lock() 87 | models.CurrentWriteReq-- 88 | models.StateLock.Unlock() 89 | } 90 | -------------------------------------------------------------------------------- /apis/handlers/addprog_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | "github.com/l3af-project/l3afd/v2/config" 13 | "github.com/l3af-project/l3afd/v2/models" 14 | ) 15 | 16 | const dummypayload string = `[ 17 | { 18 | "host_name" : "l3af-local-test", 19 | "iface" : "fakeif0", 20 | "bpf_programs" : { 21 | "xdp_ingress" : [ 22 | ], 23 | "tc_egress": [ 24 | ], 25 | "tc_ingress": [ 26 | ] 27 | } 28 | } 29 | ] 30 | ` 31 | 32 | func Test_addprog(t *testing.T) { 33 | 34 | tests := []struct { 35 | name string 36 | Body *strings.Reader 37 | header map[string]string 38 | status int 39 | cfg *bpfprogs.NFConfigs 40 | isreadonly bool 41 | }{ 42 | { 43 | name: "NilBody", 44 | Body: nil, 45 | status: http.StatusOK, 46 | isreadonly: false, 47 | cfg: &bpfprogs.NFConfigs{ 48 | HostConfig: &config.Config{ 49 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 50 | }, 51 | }, 52 | }, 53 | { 54 | name: "FailedToUnmarshal", 55 | Body: strings.NewReader("Something"), 56 | status: http.StatusInternalServerError, 57 | header: map[string]string{}, 58 | isreadonly: false, 59 | cfg: &bpfprogs.NFConfigs{ 60 | HostConfig: &config.Config{ 61 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 62 | }, 63 | }, 64 | }, 65 | { 66 | name: "EmptyInput", 67 | Body: strings.NewReader("[]"), 68 | header: map[string]string{ 69 | "Content-Type": "application/json", 70 | }, 71 | isreadonly: false, 72 | cfg: &bpfprogs.NFConfigs{ 73 | HostConfig: &config.Config{ 74 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 75 | }, 76 | }, 77 | status: http.StatusOK, 78 | }, 79 | { 80 | name: "UnknownHostName", 81 | Body: strings.NewReader(dummypayload), 82 | status: http.StatusInternalServerError, 83 | header: map[string]string{}, 84 | cfg: &bpfprogs.NFConfigs{ 85 | HostName: "dummy", 86 | HostConfig: &config.Config{ 87 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 88 | }, 89 | }, 90 | isreadonly: false, 91 | }, 92 | { 93 | name: "InReadonly", 94 | Body: nil, 95 | header: map[string]string{ 96 | "Content-Type": "application/json", 97 | }, 98 | isreadonly: true, 99 | cfg: nil, 100 | status: http.StatusOK, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | var req *http.Request 105 | if tt.Body == nil { 106 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/add", nil) 107 | } else { 108 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/add", tt.Body) 109 | } 110 | for key, val := range tt.header { 111 | req.Header.Set(key, val) 112 | } 113 | models.IsReadOnly = tt.isreadonly 114 | rr := httptest.NewRecorder() 115 | handler := AddEbpfPrograms(context.Background(), tt.cfg) 116 | handler.ServeHTTP(rr, req) 117 | if rr.Code != tt.status { 118 | models.IsReadOnly = false 119 | t.Error("AddEbpfPrograms Failed") 120 | } 121 | models.IsReadOnly = false 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /apis/handlers/deleteprog.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | 12 | "net/http" 13 | 14 | "github.com/rs/zerolog/log" 15 | 16 | "github.com/l3af-project/l3afd/v2/bpfprogs" 17 | "github.com/l3af-project/l3afd/v2/models" 18 | ) 19 | 20 | // DeleteEbpfPrograms remove eBPF programs on node 21 | // @Summary Removes eBPF Programs on node 22 | // @Description Removes eBPF Programs on node 23 | // @Accept json 24 | // @Produce json 25 | // @Param cfgs body []models.L3afBPFProgramNames true "BPF program names" 26 | // @Success 200 27 | // @Router /l3af/configs/v1/delete [post] 28 | func DeleteEbpfPrograms(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { 29 | 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | mesg := "" 32 | statusCode := http.StatusOK 33 | 34 | w.Header().Add("Content-Type", "application/json") 35 | 36 | defer func(mesg *string, statusCode *int) { 37 | w.WriteHeader(*statusCode) 38 | _, err := w.Write([]byte(*mesg)) 39 | if err != nil { 40 | log.Warn().Msgf("Failed to write response bytes: %v", err) 41 | } 42 | }(&mesg, &statusCode) 43 | if models.IsReadOnly { 44 | log.Warn().Msgf("We are in between restart please try after some time") 45 | mesg = "We are currently in the middle of a restart. Please attempt again after a while." 46 | return 47 | } 48 | defer DecWriteReq() 49 | IncWriteReq() 50 | if r.Body == nil { 51 | log.Warn().Msgf("Empty request body") 52 | return 53 | } 54 | bodyBuffer, err := io.ReadAll(r.Body) 55 | if err != nil { 56 | mesg = fmt.Sprintf("failed to read request body: %v", err) 57 | log.Error().Msg(mesg) 58 | statusCode = http.StatusInternalServerError 59 | return 60 | } 61 | 62 | var t []models.L3afBPFProgramNames 63 | if err := json.Unmarshal(bodyBuffer, &t); err != nil { 64 | mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) 65 | log.Error().Msg(mesg) 66 | statusCode = http.StatusInternalServerError 67 | return 68 | } 69 | 70 | if err := bpfcfg.DeleteEbpfPrograms(t); err != nil { 71 | mesg = fmt.Sprintf("failed to DeleteEbpfPrograms : %v", err) 72 | log.Error().Msg(mesg) 73 | 74 | statusCode = http.StatusInternalServerError 75 | return 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /apis/handlers/deleteprog_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | "github.com/l3af-project/l3afd/v2/config" 13 | "github.com/l3af-project/l3afd/v2/models" 14 | ) 15 | 16 | const payloadfordelete string = `[ 17 | { 18 | "host_name": "l3af-local-test", 19 | "iface": "fakeif0", 20 | "bpf_programs": { 21 | "xdp_ingress": [ 22 | "ratelimiting", 23 | "connection-limit" 24 | ] 25 | } 26 | } 27 | ] 28 | ` 29 | 30 | func Test_DeleteEbpfPrograms(t *testing.T) { 31 | 32 | tests := []struct { 33 | name string 34 | Body *strings.Reader 35 | header map[string]string 36 | status int 37 | cfg *bpfprogs.NFConfigs 38 | isreadonly bool 39 | }{ 40 | { 41 | name: "NilBody", 42 | Body: nil, 43 | status: http.StatusOK, 44 | isreadonly: false, 45 | cfg: &bpfprogs.NFConfigs{ 46 | HostConfig: &config.Config{ 47 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "FailedToUnmarshal", 53 | Body: strings.NewReader("Something"), 54 | status: http.StatusInternalServerError, 55 | header: map[string]string{}, 56 | isreadonly: false, 57 | cfg: &bpfprogs.NFConfigs{ 58 | HostConfig: &config.Config{ 59 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 60 | }, 61 | }, 62 | }, 63 | { 64 | name: "EmptyInput", 65 | Body: strings.NewReader(`[]`), 66 | header: map[string]string{ 67 | "Content-Type": "application/json", 68 | }, 69 | isreadonly: false, 70 | cfg: &bpfprogs.NFConfigs{ 71 | HostConfig: &config.Config{ 72 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 73 | }, 74 | }, 75 | status: http.StatusOK, 76 | }, 77 | { 78 | name: "UnknownHostName", 79 | Body: strings.NewReader(payloadfordelete), 80 | status: http.StatusInternalServerError, 81 | header: map[string]string{}, 82 | isreadonly: false, 83 | cfg: &bpfprogs.NFConfigs{ 84 | HostName: "dummy", 85 | HostConfig: &config.Config{ 86 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 87 | }, 88 | }, 89 | }, 90 | { 91 | name: "InReadonly", 92 | Body: nil, 93 | status: http.StatusOK, 94 | header: map[string]string{ 95 | "Content-Type": "application/json", 96 | }, 97 | isreadonly: true, 98 | cfg: nil, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | var req *http.Request 103 | if tt.Body == nil { 104 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/delete", nil) 105 | } else { 106 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/delete", tt.Body) 107 | } 108 | for key, val := range tt.header { 109 | req.Header.Set(key, val) 110 | } 111 | models.IsReadOnly = tt.isreadonly 112 | rr := httptest.NewRecorder() 113 | handler := DeleteEbpfPrograms(context.Background(), tt.cfg) 114 | handler.ServeHTTP(rr, req) 115 | if rr.Code != tt.status { 116 | models.IsReadOnly = false 117 | t.Error("DeleteEbpfPrograms Failed") 118 | } 119 | models.IsReadOnly = false 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /apis/handlers/getconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "encoding/json" 8 | "net/http" 9 | 10 | chi "github.com/go-chi/chi/v5" 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | var bpfcfgs *bpfprogs.NFConfigs 16 | 17 | func InitConfigs(cfgs *bpfprogs.NFConfigs) error { 18 | bpfcfgs = cfgs 19 | return nil 20 | } 21 | 22 | // GetConfig Returns details of the configuration of eBPF Programs for a given interface 23 | // @Summary Returns details of the configuration of eBPF Programs for a given interface 24 | // @Description Returns details of the configuration of eBPF Programs for a given interface 25 | // @Accept json 26 | // @Produce json 27 | // @Param iface path string true "interface name" 28 | // @Success 200 29 | // @Router /l3af/configs/v1/{iface} [get] 30 | func GetConfig(w http.ResponseWriter, r *http.Request) { 31 | mesg := "" 32 | statusCode := http.StatusOK 33 | 34 | w.Header().Add("Content-Type", "application/json") 35 | 36 | defer func(mesg *string, statusCode *int) { 37 | w.WriteHeader(*statusCode) 38 | _, err := w.Write([]byte(*mesg)) 39 | if err != nil { 40 | log.Warn().Msgf("Failed to write response bytes: %v", err) 41 | } 42 | }(&mesg, &statusCode) 43 | 44 | iface := chi.URLParam(r, "iface") 45 | if len(iface) == 0 { 46 | mesg = "iface value is empty" 47 | log.Error().Msg(mesg) 48 | statusCode = http.StatusBadRequest 49 | return 50 | } 51 | 52 | resp, err := json.MarshalIndent(bpfcfgs.EBPFPrograms(iface), "", " ") 53 | if err != nil { 54 | mesg = "internal server error" 55 | log.Error().Msgf("failed to marshal response: %v", err) 56 | statusCode = http.StatusInternalServerError 57 | return 58 | } 59 | mesg = string(resp) 60 | } 61 | 62 | // GetConfigAll Returns details of the configuration of eBPF Programs for all interfaces on a node 63 | // @Summary Returns details of the configuration of eBPF Programs for all interfaces on a node 64 | // @Description Returns details of the configuration of eBPF Programs for all interfaces on a node 65 | // @Accept json 66 | // @Produce json 67 | // @Success 200 68 | // @Router /l3af/configs/v1 [get] 69 | func GetConfigAll(w http.ResponseWriter, r *http.Request) { 70 | mesg := "" 71 | statusCode := http.StatusOK 72 | 73 | w.Header().Add("Content-Type", "application/json") 74 | 75 | defer func(mesg *string, statusCode *int) { 76 | w.WriteHeader(*statusCode) 77 | _, err := w.Write([]byte(*mesg)) 78 | if err != nil { 79 | log.Warn().Msgf("Failed to write response bytes: %v", err) 80 | } 81 | }(&mesg, &statusCode) 82 | 83 | resp, err := json.MarshalIndent(bpfcfgs.EBPFProgramsAll(), "", " ") 84 | if err != nil { 85 | mesg = "internal server error" 86 | log.Error().Msgf("failed to marshal response: %v", err) 87 | statusCode = http.StatusInternalServerError 88 | return 89 | } 90 | mesg = string(resp) 91 | } 92 | -------------------------------------------------------------------------------- /apis/handlers/getconfig_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "container/list" 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | chi "github.com/go-chi/chi/v5" 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | ) 13 | 14 | func Test_GetConfig(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | iface string 18 | status int 19 | cfg *bpfprogs.NFConfigs 20 | }{ 21 | { 22 | name: "EmptyInterfaceInRequest", 23 | iface: "", 24 | status: http.StatusBadRequest, 25 | cfg: &bpfprogs.NFConfigs{}, 26 | }, 27 | { 28 | name: "GoodInput", 29 | iface: "fakeif0", 30 | status: http.StatusOK, 31 | cfg: &bpfprogs.NFConfigs{ 32 | IngressXDPBpfs: map[string]*list.List{"fakeif0": nil}, 33 | IngressTCBpfs: map[string]*list.List{"fakeif0": nil}, 34 | EgressTCBpfs: map[string]*list.List{"fakeif0": nil}, 35 | }, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | req, _ := http.NewRequest("GET", "l3af/configs/v1/"+tt.iface, nil) 40 | rctx := chi.NewRouteContext() 41 | req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) 42 | rctx.URLParams.Add("iface", tt.iface) 43 | rr := httptest.NewRecorder() 44 | handler := http.HandlerFunc(GetConfig) 45 | InitConfigs(tt.cfg) 46 | handler.ServeHTTP(rr, req) 47 | if rr.Code != tt.status { 48 | t.Errorf("GetConfig Failed") 49 | } 50 | } 51 | } 52 | 53 | func Test_GetConfigAll(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | status int 57 | cfg *bpfprogs.NFConfigs 58 | }{ 59 | { 60 | name: "GoodInput", 61 | status: http.StatusOK, 62 | cfg: &bpfprogs.NFConfigs{}, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | req, _ := http.NewRequest("GET", "l3af/configs/v1", nil) 67 | rr := httptest.NewRecorder() 68 | handler := http.HandlerFunc(GetConfigAll) 69 | InitConfigs(tt.cfg) 70 | handler.ServeHTTP(rr, req) 71 | if rr.Code != tt.status { 72 | t.Errorf("GetConfigAll Failed") 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apis/handlers/restart_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "encoding/gob" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | "syscall" 18 | "time" 19 | 20 | "net/http" 21 | 22 | "github.com/rs/zerolog/log" 23 | 24 | "github.com/l3af-project/l3afd/v2/bpfprogs" 25 | "github.com/l3af-project/l3afd/v2/models" 26 | "github.com/l3af-project/l3afd/v2/pidfile" 27 | "github.com/l3af-project/l3afd/v2/restart" 28 | ) 29 | 30 | // HandleRestart will start new instance of l3afd provided by payload 31 | // @Summary this api will start new instance of l3afd provided by payload 32 | // @Description this api will start new instance of l3afd provided by payload 33 | // @Accept json 34 | // @Produce json 35 | // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" 36 | // @Success 200 37 | // @Router /l3af/configs/v1/restart [put] 38 | func HandleRestart(bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | mesg := "" 41 | statusCode := http.StatusOK 42 | w.Header().Add("Content-Type", "application/json") 43 | defer func(mesg *string, statusCode *int) { 44 | w.WriteHeader(*statusCode) 45 | _, err := w.Write([]byte(*mesg)) 46 | if err != nil { 47 | log.Warn().Msgf("Failed to write response bytes: %v", err) 48 | } 49 | }(&mesg, &statusCode) 50 | if models.IsReadOnly { 51 | log.Warn().Msgf("We are in between restart please try after some time") 52 | mesg = "We are currently in the middle of a restart. Please attempt again after a while." 53 | statusCode = http.StatusInternalServerError 54 | return 55 | } 56 | 57 | if r.Body == nil { 58 | mesg = "nil request body" 59 | statusCode = http.StatusInternalServerError 60 | log.Warn().Msgf("Empty request body") 61 | return 62 | } 63 | bodyBuffer, err := io.ReadAll(r.Body) 64 | if err != nil { 65 | mesg = fmt.Sprintf("failed to read request body: %v", err) 66 | log.Error().Msg(mesg) 67 | statusCode = http.StatusInternalServerError 68 | return 69 | } 70 | 71 | var t models.RestartConfig 72 | if err := json.Unmarshal(bodyBuffer, &t); err != nil { 73 | mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) 74 | log.Error().Msg(mesg) 75 | statusCode = http.StatusInternalServerError 76 | return 77 | } 78 | 79 | machineHostname, err := os.Hostname() 80 | if err != nil { 81 | mesg = "failed to get os hostname" 82 | log.Error().Msg(mesg) 83 | statusCode = http.StatusInternalServerError 84 | return 85 | } 86 | if machineHostname != t.HostName { 87 | mesg = "this api request is not for provided host" 88 | log.Error().Msg(mesg) 89 | statusCode = http.StatusInternalServerError 90 | return 91 | } 92 | 93 | defer func() { 94 | models.IsReadOnly = false 95 | }() 96 | models.IsReadOnly = true 97 | 98 | // complete active requests 99 | for { 100 | models.StateLock.Lock() 101 | if models.CurrentWriteReq == 0 { 102 | models.StateLock.Unlock() 103 | break 104 | } 105 | models.StateLock.Unlock() 106 | time.Sleep(5 * time.Millisecond) 107 | } 108 | // Now our system is in Readonly state 109 | 110 | oldCfgPath, err := restart.ReadSymlink(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg")) 111 | if err != nil { 112 | mesg = fmt.Sprintf("failed read symlink %v with error: %v", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg"), err) 113 | log.Error().Msg(mesg) 114 | statusCode = http.StatusInternalServerError 115 | return 116 | } 117 | oldBinPath, err := restart.ReadSymlink(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd")) 118 | if err != nil { 119 | mesg = fmt.Sprintf("failed read symlink %v with error: %v", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd"), err) 120 | log.Error().Msg(mesg) 121 | statusCode = http.StatusInternalServerError 122 | return 123 | } 124 | 125 | // /usr/local/l3afd/v2.0.0/l3afd/l3afd --> v2.0.0/l3afd/l3afd --> v2.0.0 126 | oldVersion := strings.Split(strings.Trim(oldBinPath, bpfcfg.HostConfig.BasePath+"/"), "/")[0] 127 | if _, ok := models.AvailableVersions[t.Version]; !ok { 128 | mesg = "invalid version to upgrade" 129 | log.Error().Msg(mesg) 130 | statusCode = http.StatusInternalServerError 131 | return 132 | } 133 | err = restart.GetNewVersion(models.L3AFDRestartArtifactName, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 134 | if err != nil { 135 | mesg = fmt.Sprintf("failed to get new version: %v", err) 136 | log.Error().Msg(mesg) 137 | statusCode = http.StatusInternalServerError 138 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 139 | mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) 140 | return 141 | } 142 | 143 | bpfProgs := bpfcfg.GetL3AFHOSTDATA() 144 | ln, err := net.Listen("unix", models.HostSock) 145 | if err != nil { 146 | log.Err(err) 147 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 148 | mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) 149 | statusCode = http.StatusInternalServerError 150 | return 151 | } 152 | srvError := make(chan error, 1) 153 | go func() { 154 | defer ln.Close() 155 | conn, err := ln.Accept() 156 | if err != nil { 157 | log.Err(err) 158 | srvError <- err 159 | return 160 | } 161 | defer conn.Close() 162 | encoder := gob.NewEncoder(conn) 163 | err = encoder.Encode(bpfProgs) 164 | if err != nil { 165 | log.Err(err) 166 | srvError <- err 167 | return 168 | } 169 | srvError <- nil 170 | }() 171 | 172 | files := make([]*os.File, 3) 173 | srvToIndex := make(map[string]int) 174 | srvToIndex["stat_http"] = 0 175 | srvToIndex["main_http"] = 1 176 | srvToIndex["debug_http"] = 2 177 | isErr := false 178 | models.AllNetListeners.Range(func(srvr, listr interface{}) bool { // iterate over the map 179 | srv, _ := srvr.(string) 180 | lis, _ := listr.(*net.TCPListener) 181 | idx := srvToIndex[srv] 182 | lf, err := lis.File() 183 | if err != nil { 184 | log.Error().Msgf("%v", err) 185 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 186 | mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) 187 | statusCode = http.StatusInternalServerError 188 | isErr = true 189 | return false 190 | } 191 | newFile := os.NewFile(uintptr(lf.Fd()), "dupFdlistner"+strconv.Itoa(idx)) 192 | files[idx] = newFile 193 | return true 194 | }) 195 | if isErr { 196 | return 197 | } 198 | 199 | cmd := exec.Command(filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd"), "--config", filepath.Join(bpfcfg.HostConfig.BasePath, "latest/l3afd.cfg")) 200 | cmd.SysProcAttr = &syscall.SysProcAttr{ 201 | Setsid: true, 202 | } 203 | cmd.Stdout = os.Stdout 204 | cmd.Stderr = os.Stderr 205 | cmd.ExtraFiles = files 206 | 207 | err = bpfcfg.StopAllProbesAndUserPrograms() 208 | if err != nil { 209 | log.Err(err) 210 | err = bpfcfg.StartAllUserProgramsAndProbes() 211 | if err != nil { 212 | log.Error().Msgf("%v", err) 213 | mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) 214 | } 215 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 216 | mesg = mesg + fmt.Sprintf("rollback of symlinks failed: %v", err) 217 | statusCode = http.StatusInternalServerError 218 | return 219 | } 220 | 221 | log.Info().Msg("Starting child Process") 222 | err = cmd.Start() 223 | if err != nil { 224 | log.Error().Msgf("%v", err) 225 | mesg = mesg + fmt.Sprintf("unable to start new instance %v", err) 226 | err = cmd.Process.Kill() 227 | if err != nil { 228 | log.Error().Msgf("%v", err) 229 | mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) 230 | } 231 | err = bpfcfg.StartAllUserProgramsAndProbes() 232 | if err != nil { 233 | log.Error().Msgf("%v", err) 234 | mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) 235 | } 236 | err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) 237 | if err != nil { 238 | log.Error().Msgf("%v", err) 239 | mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) 240 | } 241 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 242 | if err != nil { 243 | mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) 244 | } 245 | statusCode = http.StatusInternalServerError 246 | return 247 | } 248 | NewProcessStatus := make(chan string) 249 | go func() { 250 | // I need to write client code for reading the state of new process 251 | var err error 252 | var conn net.Conn 253 | f := false 254 | for i := 1; i <= bpfcfg.HostConfig.TimetoRestart; i++ { 255 | conn, err = net.Dial("unix", models.StateSock) 256 | if err == nil { 257 | f = true 258 | break 259 | } 260 | log.Info().Msgf("Waiting for socket to be up...") 261 | time.Sleep(time.Second) // sleep for a second before trying again 262 | } 263 | if !f { 264 | conn.Close() 265 | NewProcessStatus <- models.StatusFailed 266 | return 267 | } 268 | defer conn.Close() 269 | decoder := gob.NewDecoder(conn) 270 | var data string 271 | err = decoder.Decode(&data) 272 | if err != nil { 273 | NewProcessStatus <- models.StatusFailed 274 | return 275 | } 276 | NewProcessStatus <- data 277 | }() 278 | 279 | // time to bootup 280 | select { 281 | case terr := <-srvError: 282 | if terr != nil { 283 | statusCode = http.StatusInternalServerError 284 | err = cmd.Process.Kill() 285 | if err != nil { 286 | log.Error().Msgf("%v", err) 287 | mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) 288 | } 289 | err = bpfcfg.StartAllUserProgramsAndProbes() 290 | if err != nil { 291 | log.Error().Msgf("%v", err) 292 | mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) 293 | } 294 | err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) 295 | if err != nil { 296 | log.Error().Msgf("%v", err) 297 | mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) 298 | } 299 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 300 | if err != nil { 301 | mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) 302 | } 303 | statusCode = http.StatusInternalServerError 304 | log.Err(terr) 305 | return 306 | } 307 | break 308 | default: 309 | time.Sleep(time.Second) 310 | } 311 | 312 | st := <-NewProcessStatus 313 | if st == models.StatusFailed { 314 | err = cmd.Process.Kill() 315 | if err != nil { 316 | log.Error().Msgf("%v", err) 317 | mesg = mesg + fmt.Sprintf("unable to kill the new instance %v", err) 318 | } 319 | err = bpfcfg.StartAllUserProgramsAndProbes() 320 | if err != nil { 321 | log.Error().Msgf("%v", err) 322 | mesg = mesg + fmt.Sprintf("unable to start all userprograms and probes: %v", err) 323 | } 324 | err = pidfile.CreatePID(bpfcfg.HostConfig.PIDFilename) 325 | if err != nil { 326 | log.Error().Msgf("%v", err) 327 | mesg = mesg + fmt.Sprintf("unable to create pid file: %v", err) 328 | } 329 | err = restart.RollBackSymlink(oldCfgPath, oldBinPath, oldVersion, models.AvailableVersions[t.Version], bpfcfg.HostConfig) 330 | if err != nil { 331 | mesg = mesg + fmt.Sprintf("rollback of symlink failed: %v", err) 332 | } 333 | statusCode = http.StatusInternalServerError 334 | return 335 | } else { 336 | log.Info().Msgf("doing exiting old process") 337 | models.CloseForRestart <- struct{}{} 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /apis/handlers/restart_linux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package handlers 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/l3af-project/l3afd/v2/models" 11 | ) 12 | 13 | func Test_HandleRestart(t *testing.T) { 14 | var req *http.Request 15 | req, _ = http.NewRequest("PUT", "/l3af/configs/v1/restart", nil) 16 | req.Header.Set("Content-Type", "application/json") 17 | models.IsReadOnly = true 18 | rr := httptest.NewRecorder() 19 | handler := HandleRestart(nil) 20 | handler.ServeHTTP(rr, req) 21 | if rr.Code != http.StatusInternalServerError { 22 | models.IsReadOnly = false 23 | t.Error("Handle restart Failed") 24 | } 25 | models.IsReadOnly = false 26 | } 27 | -------------------------------------------------------------------------------- /apis/handlers/restart_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/rs/zerolog/log" 10 | 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | ) 13 | 14 | // HandleRestart Store meta data about ebpf programs and exit 15 | // @Summary Store meta data about ebpf programs and exit 16 | // @Description Store meta data about ebpf programs and exit 17 | // @Accept json 18 | // @Produce json 19 | // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" 20 | // @Success 200 21 | // @Router /l3af/configs/v1/restart [put] 22 | func HandleRestart(bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | mesg := "" 25 | statusCode := http.StatusOK 26 | w.Header().Add("Content-Type", "application/json") 27 | defer func(mesg *string, statusCode *int) { 28 | w.WriteHeader(*statusCode) 29 | _, err := w.Write([]byte(*mesg)) 30 | if err != nil { 31 | log.Warn().Msgf("Failed to write response bytes: %v", err) 32 | } 33 | }(&mesg, &statusCode) 34 | mesg = "Graceful restart is only supported for linux as of now" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apis/handlers/restart_windows_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package handlers 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func Test_HandleRestart(t *testing.T) { 12 | var req *http.Request 13 | req, _ = http.NewRequest("PUT", "/l3af/configs/v1/restart", nil) 14 | req.Header.Set("Content-Type", "application/json") 15 | rr := httptest.NewRecorder() 16 | handler := HandleRestart(nil) 17 | handler.ServeHTTP(rr, req) 18 | if rr.Code != http.StatusOK { 19 | t.Error("Handle restart Failed") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apis/handlers/updateconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package handlers 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | 12 | "net/http" 13 | 14 | "github.com/rs/zerolog/log" 15 | 16 | "github.com/l3af-project/l3afd/v2/bpfprogs" 17 | "github.com/l3af-project/l3afd/v2/models" 18 | ) 19 | 20 | // UpdateConfig Update eBPF Programs configuration 21 | // @Summary Update eBPF Programs configuration 22 | // @Description Update eBPF Programs configuration 23 | // @Accept json 24 | // @Produce json 25 | // @Param cfgs body []models.L3afBPFPrograms true "BPF programs" 26 | // @Success 200 27 | // @Router /l3af/configs/v1/update [post] 28 | func UpdateConfig(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) http.HandlerFunc { 29 | 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | mesg := "" 32 | statusCode := http.StatusOK 33 | 34 | w.Header().Add("Content-Type", "application/json") 35 | 36 | defer func(mesg *string, statusCode *int) { 37 | w.WriteHeader(*statusCode) 38 | _, err := w.Write([]byte(*mesg)) 39 | if err != nil { 40 | log.Warn().Msgf("Failed to write response bytes: %v", err) 41 | } 42 | }(&mesg, &statusCode) 43 | if models.IsReadOnly { 44 | log.Warn().Msgf("We are in between restart please try after some time") 45 | mesg = "We are currently in the middle of a restart. Please attempt again after a while." 46 | return 47 | } 48 | defer DecWriteReq() 49 | IncWriteReq() 50 | if r.Body == nil { 51 | log.Warn().Msgf("Empty request body") 52 | return 53 | } 54 | bodyBuffer, err := io.ReadAll(r.Body) 55 | if err != nil { 56 | mesg = fmt.Sprintf("failed to read request body: %v", err) 57 | log.Error().Msg(mesg) 58 | statusCode = http.StatusInternalServerError 59 | return 60 | } 61 | 62 | var t []models.L3afBPFPrograms 63 | if err := json.Unmarshal(bodyBuffer, &t); err != nil { 64 | mesg = fmt.Sprintf("failed to unmarshal payload: %v", err) 65 | log.Error().Msg(mesg) 66 | statusCode = http.StatusInternalServerError 67 | return 68 | } 69 | 70 | if err := bpfcfg.DeployeBPFPrograms(t); err != nil { 71 | mesg = fmt.Sprintf("failed to deploy ebpf programs: %v", err) 72 | log.Error().Msg(mesg) 73 | statusCode = http.StatusInternalServerError 74 | return 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apis/handlers/updateconfig_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/l3af-project/l3afd/v2/bpfprogs" 12 | "github.com/l3af-project/l3afd/v2/config" 13 | "github.com/l3af-project/l3afd/v2/models" 14 | ) 15 | 16 | func Test_UpdateConfig(t *testing.T) { 17 | 18 | tests := []struct { 19 | name string 20 | Body *strings.Reader 21 | header map[string]string 22 | status int 23 | cfg *bpfprogs.NFConfigs 24 | isreadonly bool 25 | }{ 26 | { 27 | name: "NilBody", 28 | Body: nil, 29 | status: http.StatusOK, 30 | isreadonly: false, 31 | cfg: &bpfprogs.NFConfigs{ 32 | HostConfig: &config.Config{ 33 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 34 | }, 35 | }, 36 | }, 37 | { 38 | name: "FailedToUnmarshal", 39 | Body: strings.NewReader("Something"), 40 | status: http.StatusInternalServerError, 41 | header: map[string]string{}, 42 | isreadonly: false, 43 | cfg: &bpfprogs.NFConfigs{ 44 | HostConfig: &config.Config{ 45 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 46 | }, 47 | }, 48 | }, 49 | { 50 | name: "UnknownHostName", 51 | Body: strings.NewReader(dummypayload), 52 | status: http.StatusInternalServerError, 53 | header: map[string]string{}, 54 | isreadonly: false, 55 | cfg: &bpfprogs.NFConfigs{ 56 | HostName: "dummy", 57 | HostConfig: &config.Config{ 58 | L3afConfigStoreFileName: filepath.FromSlash("../../testdata/Test_l3af-config.json"), 59 | }, 60 | }, 61 | }, 62 | { 63 | name: "InReadonly", 64 | Body: nil, 65 | status: http.StatusOK, 66 | header: map[string]string{ 67 | "Content-Type": "application/json", 68 | }, 69 | isreadonly: true, 70 | cfg: nil, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | var req *http.Request 75 | if tt.Body == nil { 76 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/update", nil) 77 | } else { 78 | req, _ = http.NewRequest("POST", "/l3af/configs/v1/update", tt.Body) 79 | } 80 | for key, val := range tt.header { 81 | req.Header.Set(key, val) 82 | } 83 | models.IsReadOnly = tt.isreadonly 84 | rr := httptest.NewRecorder() 85 | handler := UpdateConfig(context.Background(), tt.cfg) 86 | handler.ServeHTTP(rr, req) 87 | if rr.Code != tt.status { 88 | models.IsReadOnly = false 89 | t.Error("UpdateConfig Failed") 90 | } 91 | models.IsReadOnly = false 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apis/routes.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package apis 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/l3af-project/l3afd/v2/apis/handlers" 10 | "github.com/l3af-project/l3afd/v2/bpfprogs" 11 | "github.com/l3af-project/l3afd/v2/routes" 12 | ) 13 | 14 | func apiRoutes(ctx context.Context, bpfcfg *bpfprogs.NFConfigs) []routes.Route { 15 | 16 | r := []routes.Route{ 17 | { 18 | Method: "POST", 19 | Path: "/l3af/configs/{version}/update", 20 | HandlerFunc: handlers.UpdateConfig(ctx, bpfcfg), 21 | }, 22 | { 23 | Method: "GET", 24 | Path: "/l3af/configs/{version}/{iface}", 25 | HandlerFunc: handlers.GetConfig, 26 | }, 27 | { 28 | Method: "GET", 29 | Path: "/l3af/configs/{version}", 30 | HandlerFunc: handlers.GetConfigAll, 31 | }, 32 | { 33 | Method: "POST", 34 | Path: "/l3af/configs/{version}/add", 35 | HandlerFunc: handlers.AddEbpfPrograms(ctx, bpfcfg), 36 | }, 37 | { 38 | Method: "POST", 39 | Path: "/l3af/configs/{version}/delete", 40 | HandlerFunc: handlers.DeleteEbpfPrograms(ctx, bpfcfg), 41 | }, 42 | { 43 | Method: "PUT", 44 | Path: "/l3af/configs/{version}/restart", 45 | HandlerFunc: handlers.HandleRestart(bpfcfg), 46 | }, 47 | } 48 | 49 | return r 50 | } 51 | -------------------------------------------------------------------------------- /bpfprogs/bpfCfgs_internal.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !configs 5 | // +build !configs 6 | 7 | // This file is used for walmart internal to run BPF specific configs. 8 | // We will be removing this file in future. 9 | 10 | package bpfprogs 11 | 12 | import ( 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | func (b *BPF) RunBPFConfigs() error { 17 | log.Warn().Msg("Implement custom BPF specific configs") 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /bpfprogs/bpf_test_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !WINDOWS 5 | // +build !WINDOWS 6 | 7 | package bpfprogs 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | ) 13 | 14 | func GetTestNonexecutablePathName() string { 15 | return "/var/log/syslog" 16 | } 17 | 18 | func GetTestExecutablePathName() string { 19 | return "/bin/date" 20 | } 21 | 22 | func GetTestExecutablePath() string { 23 | return "/bin" 24 | } 25 | 26 | func GetTestExecutableName() string { 27 | return "date" 28 | } 29 | 30 | // assertExecutable checks for executable permissions 31 | func assertExecutable(fPath string) error { 32 | info, err := os.Stat(fPath) 33 | if err != nil { 34 | return fmt.Errorf("could not stat file: %s with error: %w", fPath, err) 35 | } 36 | 37 | if (info.Mode()&os.ModePerm)&os.FileMode(executePerm) == 0 { 38 | return fmt.Errorf("file: %s, is not executable", fPath) 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /bpfprogs/bpf_test_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build WINDOWS 5 | // +build WINDOWS 6 | 7 | package bpfprogs 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | func GetTestNonexecutablePathName() string { 16 | return "c:/windows/system32/drivers/etc/host" 17 | } 18 | 19 | func GetTestExecutablePathName() string { 20 | return "c:/windows/system32/net.exe" 21 | } 22 | 23 | func GetTestExecutablePath() string { 24 | return "c:/windows/system32" 25 | } 26 | 27 | func GetTestExecutableName() string { 28 | return "net.exe" 29 | } 30 | 31 | // assertExecutable checks for executable permissions 32 | func assertExecutable(fPath string) error { 33 | _, err := os.Stat(fPath) 34 | if err != nil { 35 | return fmt.Errorf("could not stat file: %s with error: %v", fPath, err) 36 | } 37 | 38 | // info.Mode() does not return the correct permissions on Windows, 39 | // it always has the 'x' permissions clear, so instead use the file suffix. 40 | if !strings.HasSuffix(fPath, ".exe") { 41 | return fmt.Errorf("file: %s, is not executable", fPath) 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /bpfprogs/bpf_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build WINDOWS 5 | // +build WINDOWS 6 | 7 | // Package bpfprogs provides primitives for l3afd's network function configs. 8 | package bpfprogs 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "os" 14 | 15 | "github.com/cilium/ebpf" 16 | ) 17 | 18 | // DisableLRO - XDP programs are failing when Large Receive Offload is enabled, to fix this we use to manually disable. 19 | func DisableLRO(ifaceName string) error { 20 | return nil 21 | } 22 | 23 | // Set process resource limits only non-zero value 24 | func (b *BPF) SetPrLimits() error { 25 | if b.Cmd == nil { 26 | return errors.New("no Process to set limits") 27 | } 28 | return nil 29 | } 30 | 31 | // VerifyNMountBPFFS - Mounting bpf filesystem 32 | func VerifyNMountBPFFS() error { 33 | return nil 34 | } 35 | 36 | func GetPlatform() (string, error) { 37 | return "Windows", nil 38 | } 39 | 40 | func IsProcessRunning(pid int, name string) (bool, error) { 41 | _, err := os.FindProcess(pid) 42 | if err != nil { 43 | return false, fmt.Errorf("BPF Program not running %s because of error: %w", name, err) 44 | } 45 | return true, nil 46 | } 47 | 48 | // ProcessTerminate - Kills the process 49 | func (b *BPF) ProcessTerminate() error { 50 | if err := b.Cmd.Process.Kill(); err != nil { 51 | return fmt.Errorf("BPFProgram %s kill failed with error: %w", b.Program.Name, err) 52 | } 53 | return nil 54 | } 55 | 56 | // VerifyNCreateTCDirs - Creating BPF sudo FS for pinning TC maps 57 | func VerifyNCreateTCDirs() error { 58 | return nil 59 | } 60 | 61 | // LoadTCAttachProgram - not implemented in windows 62 | func (b *BPF) LoadTCAttachProgram(ifaceName, direction string) error { 63 | // not implement nothing todo 64 | return fmt.Errorf("LoadTCAttachProgram - TC programs Unsupported on windows") 65 | } 66 | 67 | // LoadTCXAttachProgram - not implemented in windows 68 | func (b *BPF) LoadTCXAttachProgram(ifaceName, direction string) error { 69 | // not implement nothing todo 70 | return fmt.Errorf("LoadTCXAttachProgram - TC programs Unsupported on windows") 71 | } 72 | 73 | // UnloadTCProgram - Remove TC filters 74 | func (b *BPF) UnloadTCProgram(ifaceName, direction string) error { 75 | // not implement nothing todo 76 | return fmt.Errorf("UnloadTCProgram - TC programs Unsupported on windows") 77 | } 78 | 79 | // LoadXDPAttachProgram - Attaches XDP program to interface 80 | func (b *BPF) LoadXDPAttachProgram(ifaceName string) error { 81 | // not implement nothing todo 82 | return fmt.Errorf("LoadXDPAttachProgram - AttachXDP method is Unsupported on windows") 83 | } 84 | 85 | // LoadBPFProgramProbeType - Loads Probe type bpf program 86 | func (b *BPF) LoadBPFProgramProbeTypes(objSpec *ebpf.CollectionSpec) error { 87 | // not implement nothing todo 88 | return fmt.Errorf("LoadBPFProgramProbeTypes - Probes are Unsupported on windows") 89 | } 90 | -------------------------------------------------------------------------------- /bpfprogs/bpfdebug.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package bpfprogs 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "net" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/l3af-project/l3afd/v2/models" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | var bpfcfgs *NFConfigs 18 | 19 | func SetupBPFDebug(ebpfChainDebugAddr string, BPFConfigs *NFConfigs) { 20 | bpfcfgs = BPFConfigs 21 | go func() { 22 | if _, ok := models.AllNetListeners.Load("debug_http"); !ok { 23 | tcpAddr, err := net.ResolveTCPAddr("tcp", ebpfChainDebugAddr) 24 | if err != nil { 25 | log.Fatal().Err(err).Msgf("unable to resolve tcpaddr %v ", ebpfChainDebugAddr) 26 | return 27 | } 28 | listener, err := net.ListenTCP("tcp", tcpAddr) 29 | if err != nil { 30 | log.Fatal().Err(err).Msgf("unable to create tcp listener") 31 | } 32 | models.AllNetListeners.Store("debug_http", listener) 33 | } 34 | http.HandleFunc("/bpfs/", ViewHandler) 35 | // We just need to start a server. 36 | log.Info().Msg("Starting BPF debug server") 37 | val, _ := models.AllNetListeners.Load("debug_http") 38 | l, _ := val.(*net.TCPListener) 39 | if err := http.Serve(l, nil); !errors.Is(err, http.ErrServerClosed) { 40 | log.Fatal().Err(err).Msg("failed to start BPF chain debug server") 41 | } 42 | }() 43 | } 44 | 45 | func ViewHandler(w http.ResponseWriter, r *http.Request) { 46 | 47 | iface := strings.TrimPrefix(r.URL.Path, "/bpfs/") 48 | w.Header().Set("Content-Type", "application/json") 49 | w.WriteHeader(http.StatusOK) 50 | if err := json.NewEncoder(w).Encode(bpfcfgs.BPFDetails(iface)); err != nil { 51 | log.Err(err).Msgf("unable to serialize json") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bpfprogs/bpfmap.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package bpfprogs 5 | 6 | import ( 7 | "container/ring" 8 | "errors" 9 | "fmt" 10 | "math" 11 | "unsafe" 12 | 13 | "github.com/cilium/ebpf" 14 | "github.com/l3af-project/l3afd/v2/models" 15 | "github.com/rs/zerolog/log" 16 | ) 17 | 18 | type BPFMap struct { 19 | Name string 20 | MapID ebpf.MapID 21 | Type ebpf.MapType 22 | 23 | // BPFProg reference in case of stale map id 24 | BPFProg *BPF `json:"-"` 25 | } 26 | 27 | // This stores Metrics map details. 28 | type MetricsBPFMap struct { 29 | BPFMap 30 | Key int 31 | Values *ring.Ring 32 | Aggregator string 33 | LastValue float64 34 | } 35 | 36 | // The RemoveMissingKeys function is used to delete missing entries of eBPF maps, which are used by eBPF Programs. 37 | func (b *BPFMap) RemoveMissingKeys(args []models.KeyValue) error { 38 | ebpfMap, err := ebpf.NewMapFromID(b.MapID) 39 | if err != nil { 40 | return fmt.Errorf("access new map from ID failed %w", err) 41 | } 42 | defer ebpfMap.Close() 43 | KeyValueMap := make(map[int]bool, len(args)) 44 | for _, k := range args { 45 | KeyValueMap[k.Key] = true 46 | } 47 | var key, nextKey int 48 | for { 49 | err := ebpfMap.NextKey(unsafe.Pointer(&key), unsafe.Pointer(&nextKey)) 50 | if err != nil { 51 | if errors.Is(err, ebpf.ErrKeyNotExist) { 52 | break 53 | } else { 54 | return fmt.Errorf("get next key failed with error %w, mapid %d", err, b.MapID) 55 | } 56 | } 57 | key = nextKey 58 | _, IsKeyExists := KeyValueMap[key] 59 | if !IsKeyExists { 60 | log.Info().Msgf("removing key %v because it is missing\n", key) 61 | if err := ebpfMap.Delete(unsafe.Pointer(&key)); err != nil { 62 | return fmt.Errorf("delete key failed with error %w, mapid %d", err, b.MapID) 63 | } 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // The update function is used to update eBPF maps, which are used by eBPF programs. 70 | func (b *BPFMap) Update(key, value int) error { 71 | 72 | log.Debug().Msgf("update map name %s ID %d", b.Name, b.MapID) 73 | ebpfMap, err := ebpf.NewMapFromID(b.MapID) 74 | if err != nil { 75 | return fmt.Errorf("access new map from ID failed %w", err) 76 | } 77 | defer ebpfMap.Close() 78 | log.Info().Msgf("updating map %s key %d mapid %d", b.Name, key, b.MapID) 79 | if err := ebpfMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), 0); err != nil { 80 | return fmt.Errorf("update hash map element failed for key %d error %w", key, err) 81 | } 82 | return nil 83 | } 84 | 85 | // Get value of the map for given key 86 | // There are 2 aggregators are supported here 87 | // max-rate - this calculates delta requests / sec and stores absolute value. 88 | // avg - stores the values in the circular queue 89 | // We can implement more aggregate function as needed. 90 | func (b *MetricsBPFMap) GetValue() float64 { 91 | ebpfMap, err := ebpf.NewMapFromID(b.MapID) 92 | if err != nil { 93 | // We have observed in smaller configuration VM's, if we restart BPF's 94 | // Stale mapID's are reported, in such cases re-checking map id 95 | log.Warn().Err(err).Msgf("GetValue : NewMapFromID failed ID %d, re-looking up of map id", b.MapID) 96 | tmpBPF, err := b.BPFProg.GetBPFMap(b.Name) 97 | if err != nil { 98 | log.Warn().Err(err).Msgf("GetValue: Update new map ID %d", tmpBPF.MapID) 99 | return 0 100 | } 101 | log.Info().Msgf("GetValue: Update new map ID %d", tmpBPF.MapID) 102 | b.MapID = tmpBPF.MapID 103 | ebpfMap, err = ebpf.NewMapFromID(b.MapID) 104 | if err != nil { 105 | log.Warn().Err(err).Msgf("GetValue : retry of NewMapFromID failed ID %d", b.MapID) 106 | return 0 107 | } 108 | } 109 | defer ebpfMap.Close() 110 | 111 | var value int64 112 | if err = ebpfMap.Lookup(unsafe.Pointer(&b.Key), unsafe.Pointer(&value)); err != nil { 113 | log.Warn().Err(err).Msgf("GetValue Lookup failed : Name %s ID %d", b.Name, b.MapID) 114 | return 0 115 | } 116 | 117 | var retVal float64 118 | switch b.Aggregator { 119 | case "scalar": 120 | retVal = float64(value) 121 | case "max-rate": 122 | b.Values = b.Values.Next() 123 | b.Values.Value = math.Abs(float64(float64(value) - b.LastValue)) 124 | b.LastValue = float64(value) 125 | retVal = b.MaxValue() 126 | case "avg": 127 | b.Values.Value = value 128 | b.Values = b.Values.Next() 129 | retVal = b.AvgValue() 130 | default: 131 | log.Warn().Msgf("unsupported aggregator %s and value %d", b.Aggregator, value) 132 | } 133 | return retVal 134 | } 135 | 136 | // This method finds the max value in the circular list 137 | func (b *MetricsBPFMap) MaxValue() float64 { 138 | tmp := b.Values 139 | var max float64 140 | for i := 0; i < b.Values.Len(); i++ { 141 | if tmp.Value != nil { 142 | val := tmp.Value.(float64) 143 | if max < val { 144 | max = val 145 | } 146 | } 147 | tmp = tmp.Next() 148 | } 149 | return max 150 | } 151 | 152 | // This method calculates the average 153 | func (b *MetricsBPFMap) AvgValue() float64 { 154 | tmp := b.Values.Next() 155 | var sum float64 156 | var n float64 = 0.0 157 | for i := 0; i < b.Values.Len(); i++ { 158 | if tmp.Value != nil { 159 | sum = sum + tmp.Value.(float64) 160 | n = n + 1 161 | } 162 | tmp = tmp.Next() 163 | } 164 | return sum / n 165 | } 166 | -------------------------------------------------------------------------------- /bpfprogs/bpfmap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package bpfprogs 5 | 6 | import ( 7 | "container/ring" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | var TestValues *ring.Ring = ring.New(10) 13 | 14 | func SetupTestValues() { 15 | a := [10]float64{8, 10, 6, 23, 4, 53, 32, 8, 2, 7} 16 | v := TestValues 17 | for i := 0; i < TestValues.Len(); i++ { 18 | v.Value = a[i] 19 | v = v.Next() 20 | } 21 | } 22 | func TestMetricsBPFMapMaxValue(t *testing.T) { 23 | type args struct { 24 | key int 25 | Values *ring.Ring 26 | aggregator string 27 | } 28 | SetupTestValues() 29 | tests := []struct { 30 | name string 31 | args args 32 | want float64 33 | wantErr bool 34 | }{ 35 | { 36 | name: "max-rate", 37 | args: args{key: 0, Values: TestValues, aggregator: "max-rate"}, 38 | want: 53, 39 | wantErr: false, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | metricsMap := &MetricsBPFMap{ 45 | Values: TestValues, 46 | Key: 0, 47 | Aggregator: tt.args.aggregator, 48 | LastValue: 0, 49 | } 50 | got := (metricsMap.MaxValue()) 51 | if !reflect.DeepEqual(got, tt.want) { 52 | t.Errorf("MaxValue() = %v, want %v", got, tt.want) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestMetricsBPFMapAvgValue(t *testing.T) { 59 | type args struct { 60 | key int 61 | Values *ring.Ring 62 | aggregator string 63 | } 64 | SetupTestValues() 65 | tests := []struct { 66 | name string 67 | args args 68 | want float64 69 | wantErr bool 70 | }{ 71 | { 72 | name: "avg", 73 | args: args{key: 0, Values: TestValues, aggregator: "avg"}, 74 | want: 15.3, 75 | wantErr: false, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | metricsMap := &MetricsBPFMap{ 81 | Values: TestValues, 82 | Key: 0, 83 | Aggregator: tt.args.aggregator, 84 | LastValue: 0, 85 | } 86 | got := (metricsMap.AvgValue()) 87 | if !reflect.DeepEqual(got, tt.want) { 88 | t.Errorf("AvgValue() = %v, want %v", got, tt.want) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /bpfprogs/bpfmetrics.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package bpfprogs provides primitives for NF process monitoring. 5 | package bpfprogs 6 | 7 | import ( 8 | "container/list" 9 | "time" 10 | 11 | "github.com/l3af-project/l3afd/v2/models" 12 | 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | type BpfMetrics struct { 17 | Chain bool 18 | Intervals int 19 | } 20 | 21 | func NewpBpfMetrics(chain bool, interval int) *BpfMetrics { 22 | m := &BpfMetrics{ 23 | Chain: chain, 24 | Intervals: interval, 25 | } 26 | return m 27 | } 28 | 29 | func (c *BpfMetrics) BpfMetricsStart(xdpProgs, ingressTCProgs, egressTCProgs map[string]*list.List, probes *list.List) { 30 | go c.BpfMetricsWorker(xdpProgs) 31 | go c.BpfMetricsWorker(ingressTCProgs) 32 | go c.BpfMetricsWorker(egressTCProgs) 33 | go c.BpfMetricsProbeWorker(probes) 34 | } 35 | 36 | func (c *BpfMetrics) BpfMetricsWorker(bpfProgs map[string]*list.List) { 37 | for range time.NewTicker(1 * time.Second).C { 38 | for ifaceName, bpfList := range bpfProgs { 39 | if bpfList == nil { // no bpf programs are running 40 | continue 41 | } 42 | for e := bpfList.Front(); e != nil; e = e.Next() { 43 | bpf := e.Value.(*BPF) 44 | if c.Chain && bpf.Program.SeqID == 0 { // do not monitor root program 45 | continue 46 | } 47 | if bpf.Program.AdminStatus == models.Disabled { 48 | continue 49 | } 50 | if err := bpf.MonitorMaps(ifaceName, c.Intervals); err != nil { 51 | log.Debug().Err(err).Msgf("pMonitor monitor maps failed - %s", bpf.Program.Name) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | func (c *BpfMetrics) BpfMetricsProbeWorker(bpfProgs *list.List) { 59 | for range time.NewTicker(1 * time.Second).C { 60 | if bpfProgs == nil { 61 | time.Sleep(time.Second) 62 | continue 63 | } 64 | for e := bpfProgs.Front(); e != nil; e = e.Next() { 65 | bpf := e.Value.(*BPF) 66 | if bpf.Program.AdminStatus == models.Disabled { 67 | continue 68 | } 69 | if err := bpf.MonitorMaps("", c.Intervals); err != nil { 70 | log.Debug().Err(err).Msgf("pMonitor probe monitor maps failed - %s", bpf.Program.Name) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bpfprogs/bpfmetrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package bpfprogs 5 | 6 | import ( 7 | "container/list" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestNewpKFMetrics(t *testing.T) { 13 | type args struct { 14 | chain bool 15 | interval int 16 | } 17 | tests := []struct { 18 | name string 19 | args args 20 | want *BpfMetrics 21 | wantErr bool 22 | }{ 23 | { 24 | name: "EmptypCheck", 25 | args: args{chain: false, interval: 0}, 26 | want: &BpfMetrics{Chain: false, Intervals: 0}, 27 | wantErr: false, 28 | }, 29 | { 30 | name: "ValidpCheck", 31 | args: args{chain: true, interval: 10}, 32 | want: &BpfMetrics{Chain: true, Intervals: 10}, 33 | wantErr: false, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | got := NewpBpfMetrics(tt.args.chain, tt.args.interval) 39 | if !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("NewKFMetrics() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func Test_BPFMetrics_Start(t *testing.T) { 47 | type fields struct { 48 | Chain bool 49 | Interval int 50 | } 51 | type args struct { 52 | IngressXDPbpfProgs map[string]*list.List 53 | IngressTCbpfProgs map[string]*list.List 54 | EgressTCbpfProgs map[string]*list.List 55 | Probes *list.List 56 | } 57 | tests := []struct { 58 | name string 59 | fields fields 60 | args args 61 | wantErr bool 62 | }{ 63 | { 64 | name: "EmptyBPF", 65 | fields: fields{Chain: true, Interval: 10}, 66 | args: args{IngressXDPbpfProgs: make(map[string]*list.List), 67 | IngressTCbpfProgs: make(map[string]*list.List), 68 | EgressTCbpfProgs: make(map[string]*list.List), 69 | }, 70 | wantErr: true, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | c := &BpfMetrics{ 76 | Chain: tt.fields.Chain, 77 | Intervals: tt.fields.Interval, 78 | } 79 | c.BpfMetricsStart(tt.args.IngressXDPbpfProgs, tt.args.IngressTCbpfProgs, tt.args.EgressTCbpfProgs, tt.args.Probes) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bpfprogs/probes.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !WINDOWS 5 | // +build !WINDOWS 6 | 7 | package bpfprogs 8 | 9 | import ( 10 | "fmt" 11 | "strings" 12 | 13 | "github.com/l3af-project/l3afd/v2/models" 14 | "github.com/l3af-project/l3afd/v2/stats" 15 | 16 | "github.com/cilium/ebpf" 17 | "github.com/cilium/ebpf/link" 18 | "github.com/rs/zerolog/log" 19 | ) 20 | 21 | func (b *BPF) LoadBPFProgramProbeType(prog *ebpf.Program, sectionName string) error { 22 | var progType, hookName, subType string 23 | 24 | switch prog.Type() { 25 | case ebpf.TracePoint: 26 | progType, hookName, subType = GetProgramSectionDetails(sectionName) 27 | tp, err := link.Tracepoint(hookName, subType, prog, nil) 28 | if err != nil { 29 | return fmt.Errorf("failed to link tracepoint sec name %s error %v", sectionName, err) 30 | } 31 | b.ProbeLinks = append(b.ProbeLinks, &tp) 32 | case ebpf.Kprobe: 33 | progType, hookName, _ = GetProgramSectionDetails(sectionName) 34 | kp, err := b.AttachProbePerfEvent(hookName, progType, prog) 35 | if err != nil { 36 | return fmt.Errorf("failed to attach perf event error %v", err) 37 | } 38 | b.ProbeLinks = append(b.ProbeLinks, &kp) 39 | default: 40 | return fmt.Errorf("un-supported probe type %s ", prog.Type()) 41 | } 42 | ebpfProgName := b.Program.Name + "_" + progType + "_" + hookName 43 | stats.Add(1, stats.BPFStartCount, ebpfProgName, "", "") 44 | return nil 45 | } 46 | 47 | // LoadBPFProgramProbeTypes - Load the BPF programs of probe types - TracePoint 48 | func (b *BPF) LoadBPFProgramProbeTypes(objSpec *ebpf.CollectionSpec) error { 49 | for i, prog := range b.ProgMapCollection.Programs { 50 | if prog.Type() == ebpf.XDP || prog.Type() == ebpf.SchedACT || prog.Type() == ebpf.SchedCLS { 51 | // skipping XDP/TC programs 52 | continue 53 | } 54 | if err := b.LoadBPFProgramProbeType(prog, objSpec.Programs[i].SectionName); err != nil { 55 | return err 56 | } 57 | for _, tmpMap := range b.Program.MonitorMaps { 58 | tmpMetricsMap := b.ProgMapCollection.Maps[tmpMap.Name] 59 | if tmpMetricsMap == nil { 60 | log.Error().Msgf("%s map is not loaded", tmpMap.Name) 61 | continue 62 | } 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // GetProgramSectionDetails returns group and name details 69 | // Section name format prog-type/hook/subtype 70 | // ret : prog-type, hook, subtype 71 | // e.g.: tracepoint/sock/inet_sock_set_state 72 | // e.g.: kprobe/sys_execve 73 | // e.g.: uprobe/:: 74 | func GetProgramSectionDetails(sectionName string) (string, string, string) { 75 | sections := strings.Split(sectionName, "/") 76 | 77 | switch strings.ToLower(sections[0]) { 78 | case models.TracePoint: 79 | return sections[0], sections[1], sections[2] 80 | case models.KProbe, models.KRetProbe: 81 | return sections[0], sections[1], "" 82 | case models.UProbe, models.URetProbe: 83 | var funcName string 84 | if len(sections) > 2 { 85 | funcName = strings.Join(sections[1:], "/") 86 | } 87 | return sections[0], funcName, "" 88 | default: 89 | return "", "", "" 90 | } 91 | } 92 | 93 | func (b *BPF) AttachProbePerfEvent(hookName, progType string, prog *ebpf.Program) (link.Link, error) { 94 | var kp link.Link 95 | var err error 96 | switch strings.ToLower(progType) { 97 | case models.KProbe: 98 | kp, err = link.Kprobe(hookName, prog, nil) 99 | if err != nil { 100 | return nil, fmt.Errorf("failed to link kprobe hook name %s error %v", hookName, err) 101 | } 102 | case models.KRetProbe: 103 | kp, err = link.Kretprobe(hookName, prog, nil) 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to link kretprobe hook name %s error %v", hookName, err) 106 | } 107 | case models.UProbe: 108 | kp, err = b.AttachUProbePerfEvent(hookName, prog) 109 | if err != nil { 110 | return nil, fmt.Errorf("failed to attach uprobe program %v", err) 111 | } 112 | case models.URetProbe: 113 | kp, err = b.AttachURetProbePerfEvent(hookName, prog) 114 | if err != nil { 115 | return nil, fmt.Errorf("failed to attach uretprobe program %v", err) 116 | } 117 | default: 118 | return nil, fmt.Errorf("unsupported perf event progType: %s", progType) 119 | } 120 | return kp, nil 121 | } 122 | 123 | func (b *BPF) AttachUProbePerfEvent(hookName string, prog *ebpf.Program) (link.Link, error) { 124 | var kp link.Link 125 | funcNames := strings.Split(hookName, ":") 126 | ex, err := link.OpenExecutable(funcNames[0]) 127 | if err != nil { 128 | return nil, fmt.Errorf("uprobe failed to openExecutable binary file %s error %v", hookName, err) 129 | } 130 | 131 | kp, err = ex.Uprobe(getSymbolName(funcNames), prog, nil) 132 | 133 | if err != nil { 134 | return nil, fmt.Errorf("failed to link uprobe symbol %s - %v", getSymbolName(funcNames), err) 135 | } 136 | 137 | return kp, nil 138 | } 139 | 140 | func (b *BPF) AttachURetProbePerfEvent(hookName string, prog *ebpf.Program) (link.Link, error) { 141 | var kp link.Link 142 | funcNames := strings.Split(hookName, ":") 143 | ex, err := link.OpenExecutable(funcNames[0]) 144 | if err != nil { 145 | return nil, fmt.Errorf("uretprobe failed to openExecutable binary file %s error %v", hookName, err) 146 | } 147 | 148 | kp, err = ex.Uretprobe(getSymbolName(funcNames), prog, nil) 149 | if err != nil { 150 | return nil, fmt.Errorf("failed to link uretprobe symbol %s - %v", getSymbolName(funcNames), err) 151 | } 152 | 153 | return kp, nil 154 | } 155 | 156 | func getSymbolName(funcNames []string) string { 157 | var symbol string 158 | if len(funcNames) == 1 { 159 | symbol = funcNames[0] 160 | } else { 161 | symbol = funcNames[len(funcNames)-1] 162 | } 163 | return symbol 164 | } 165 | -------------------------------------------------------------------------------- /bpfprogs/probes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !WINDOWS 5 | // +build !WINDOWS 6 | 7 | package bpfprogs 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestGetProgramSectionDetails(t *testing.T) { 14 | 15 | tests := []struct { 16 | desc string 17 | sectionName string 18 | want1 string 19 | want2 string 20 | want3 string 21 | }{ 22 | { 23 | desc: "empty section name", 24 | sectionName: "", 25 | want1: "", 26 | want2: "", 27 | }, 28 | { 29 | desc: "section name contains group", 30 | sectionName: "kprobe/perf_event", 31 | want1: "kprobe", 32 | want2: "perf_event", 33 | want3: "", 34 | }, 35 | { 36 | desc: "section name contains all details", 37 | sectionName: "tracepoint/sock/inet_sock_set_state", 38 | want1: "tracepoint", 39 | want2: "sock", 40 | want3: "inet_sock_set_state", 41 | }, 42 | } 43 | 44 | for _, test := range tests { 45 | test := test 46 | t.Run(test.desc, func(t *testing.T) { 47 | got1, got2, got3 := GetProgramSectionDetails(test.sectionName) 48 | if test.want1 != got1 && test.want2 != got2 && test.want3 != got3 { 49 | t.Errorf("want1 %v => got1 %v, want2 %v => got2 %v, want3 %v => got3 %v", test.want1, got1, test.want2, got2, test.want3, got3) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bpfprogs/processCheck.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package bpfprogs provides primitives for BPF process monitoring. 5 | package bpfprogs 6 | 7 | import ( 8 | "container/list" 9 | "time" 10 | 11 | "github.com/l3af-project/l3afd/v2/models" 12 | "github.com/l3af-project/l3afd/v2/stats" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | type PCheck struct { 17 | MaxRetryCount int 18 | Chain bool 19 | RetryMonitorDelay time.Duration 20 | } 21 | 22 | func NewPCheck(rc int, chain bool, interval time.Duration) *PCheck { 23 | c := &PCheck{ 24 | MaxRetryCount: rc, 25 | Chain: chain, 26 | RetryMonitorDelay: interval, 27 | } 28 | return c 29 | } 30 | 31 | func (c *PCheck) PCheckStart(xdpProgs, ingressTCProgs, egressTCProgs map[string]*list.List, probes *list.List) { 32 | go c.pMonitorWorker(xdpProgs, models.XDPIngressType) 33 | go c.pMonitorWorker(ingressTCProgs, models.IngressType) 34 | go c.pMonitorWorker(egressTCProgs, models.EgressType) 35 | go c.pMonitorProbeWorker(probes) 36 | } 37 | 38 | func (c *PCheck) pMonitorWorker(bpfProgs map[string]*list.List, direction string) { 39 | for range time.NewTicker(c.RetryMonitorDelay).C { 40 | if models.IsReadOnly { 41 | log.Info().Msgf("Not monitoring because we are in readonly state") 42 | return 43 | } 44 | for ifaceName, bpfList := range bpfProgs { 45 | if bpfList == nil { // no bpf programs are running 46 | continue 47 | } 48 | for e := bpfList.Front(); e != nil; e = e.Next() { 49 | bpf := e.Value.(*BPF) 50 | if c.Chain && bpf.Program.SeqID == 0 { // do not monitor root program 51 | continue 52 | } 53 | if bpf.Program.AdminStatus == models.Disabled { 54 | continue 55 | } 56 | userProgram, bpfProgram, _ := bpf.isRunning() 57 | if userProgram && bpfProgram { 58 | stats.SetWithVersion(1.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, direction, ifaceName) 59 | continue 60 | } 61 | // Not running trying to restart 62 | if bpf.RestartCount < c.MaxRetryCount && bpf.Program.AdminStatus == models.Enabled { 63 | bpf.RestartCount++ 64 | log.Warn().Msgf("pMonitor BPF Program is not running. Restart attempt: %d, program name: %s, iface: %s", 65 | bpf.RestartCount, bpf.Program.Name, ifaceName) 66 | // User program is a daemon and not running, but the BPF program is loaded 67 | if !userProgram && bpfProgram { 68 | if err := bpf.StartUserProgram(ifaceName, direction, c.Chain); err != nil { 69 | log.Error().Err(err).Msgf("pMonitorWorker: BPF Program start user program failed for program %s", bpf.Program.Name) 70 | } 71 | } 72 | // BPF program is not loaded. 73 | // if user program is daemon then stop it and restart both the programs 74 | if !bpfProgram { 75 | log.Warn().Msgf("%s BPF program is not loaded, %s program reloading ...", bpf.Program.EntryFunctionName, bpf.Program.Name) 76 | // User program is a daemon and running, stop before reloading the BPF program 77 | if bpf.Program.UserProgramDaemon && userProgram { 78 | if err := bpf.Stop(ifaceName, direction, c.Chain); err != nil { 79 | log.Error().Err(err).Msgf("pMonitorWorker: BPF Program stop failed for program %s", bpf.Program.Name) 80 | } 81 | } 82 | if err := bpf.Start(ifaceName, direction, c.Chain); err != nil { 83 | log.Error().Err(err).Msgf("pMonitorWorker: BPF Program start failed for program %s", bpf.Program.Name) 84 | } 85 | } 86 | } else { 87 | stats.SetWithVersion(0.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, direction, ifaceName) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | func (c *PCheck) pMonitorProbeWorker(bpfProgs *list.List) { 95 | for range time.NewTicker(c.RetryMonitorDelay).C { 96 | if bpfProgs == nil { 97 | time.Sleep(time.Second) 98 | continue 99 | } 100 | for e := bpfProgs.Front(); e != nil; e = e.Next() { 101 | bpf := e.Value.(*BPF) 102 | if bpf.Program.AdminStatus == models.Disabled { 103 | continue 104 | } 105 | userProgram, bpfProgram, _ := bpf.isRunning() 106 | if userProgram && bpfProgram { 107 | stats.SetWithVersion(1.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, "", "") 108 | continue 109 | } 110 | 111 | // Not running trying to restart 112 | if bpf.RestartCount < c.MaxRetryCount && bpf.Program.AdminStatus == models.Enabled { 113 | bpf.RestartCount++ 114 | log.Warn().Msgf("pMonitorProbeWorker: BPF Program is not running. Restart attempt: %d, program name: %s, iface: %s", 115 | bpf.RestartCount, bpf.Program.Name, "") 116 | // User program is a daemon and not running, but the BPF program is loaded 117 | if !userProgram && bpfProgram { 118 | if err := bpf.StartUserProgram("", "", c.Chain); err != nil { 119 | log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program start user program failed for program %s", bpf.Program.Name) 120 | } 121 | } 122 | // BPF program is not loaded. 123 | // if user program is daemon then stop it and restart both the programs 124 | if !bpfProgram { 125 | log.Warn().Msgf("%s BPF program is not loaded, %s program reloading ...", bpf.Program.EntryFunctionName, bpf.Program.Name) 126 | // User program is a daemon and running, stop before reloading the BPF program 127 | if bpf.Program.UserProgramDaemon && userProgram { 128 | if err := bpf.Stop("", "", c.Chain); err != nil { 129 | log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program stop failed for program %s", bpf.Program.Name) 130 | } 131 | } 132 | if err := bpf.Start("", "", c.Chain); err != nil { 133 | log.Error().Err(err).Msgf("pMonitorProbeWorker: BPF Program start failed for program %s", bpf.Program.Name) 134 | } 135 | } 136 | } else { 137 | stats.SetWithVersion(0.0, stats.BPFRunning, bpf.Program.Name, bpf.Program.Version, "", "") 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /bpfprogs/processCheck_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package bpfprogs 5 | 6 | import ( 7 | "container/list" 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewpCheck(t *testing.T) { 14 | type args struct { 15 | rc int 16 | chain bool 17 | interval time.Duration 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want *PCheck 23 | wantErr bool 24 | }{ 25 | { 26 | name: "EmptypCheck", 27 | args: args{rc: 0, chain: false, interval: 0}, 28 | want: &PCheck{MaxRetryCount: 0}, 29 | wantErr: false, 30 | }, 31 | { 32 | name: "ValidpCheck", 33 | args: args{rc: 3, chain: true, interval: 10}, 34 | want: &PCheck{MaxRetryCount: 3}, 35 | wantErr: false, 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | got := NewPCheck(tt.args.rc, false, 0) 41 | if !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("NewpCheck() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_pCheck_pCheckStart(t *testing.T) { 49 | type fields struct { 50 | MaxRetryCount int 51 | chain bool 52 | retryMonitorDelay time.Duration 53 | } 54 | type args struct { 55 | IngressXDPbpfProgs map[string]*list.List 56 | IngressTCbpfProgs map[string]*list.List 57 | EgressTCbpfProgs map[string]*list.List 58 | Probebpfs list.List 59 | } 60 | tests := []struct { 61 | name string 62 | fields fields 63 | args args 64 | wantErr bool 65 | }{ 66 | { 67 | name: "EmptyBPF", 68 | fields: fields{MaxRetryCount: 3, chain: true, retryMonitorDelay: 10}, 69 | args: args{IngressXDPbpfProgs: make(map[string]*list.List), 70 | IngressTCbpfProgs: make(map[string]*list.List), 71 | EgressTCbpfProgs: make(map[string]*list.List), 72 | }, 73 | wantErr: true, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | c := &PCheck{ 79 | MaxRetryCount: tt.fields.MaxRetryCount, 80 | Chain: tt.fields.chain, 81 | RetryMonitorDelay: tt.fields.retryMonitorDelay, 82 | } 83 | c.PCheckStart(tt.args.IngressXDPbpfProgs, tt.args.IngressTCbpfProgs, tt.args.EgressTCbpfProgs, &tt.args.Probebpfs) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /build-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:jammy@sha256:6d7b5d3317a71adb5e175640150e44b8b9a9401a7dd394f44840626aff9fa94d 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | USER root 6 | 7 | # Install necessary dependencies 8 | RUN apt-get update && \ 9 | apt-get install -y \ 10 | clang \ 11 | llvm \ 12 | libelf-dev \ 13 | linux-headers-generic \ 14 | linux-tools-common linux-tools-generic \ 15 | libbpf-dev \ 16 | tzdata \ 17 | cmake \ 18 | zlib1g-dev \ 19 | libevent-dev \ 20 | vim \ 21 | wget \ 22 | curl \ 23 | linux-tools-generic \ 24 | net-tools \ 25 | iproute2 \ 26 | elfutils \ 27 | libjson-c-dev \ 28 | curl \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | RUN mkdir -p /srv/l3afd/ 32 | RUN mkdir -p /var/l3afd 33 | RUN mkdir -p /var/log/l3af 34 | RUN mkdir -p /usr/local/l3afd/latest 35 | RUN mkdir -p /usr/local/l3afd/v2.1.0/l3afd 36 | 37 | COPY l3afd /usr/local/l3afd/v2.1.0/l3afd/l3afd 38 | COPY l3afd.cfg /usr/local/l3afd/v2.1.0/l3afd/l3afd.cfg 39 | COPY start.sh /usr/local/l3afd/start.sh 40 | 41 | RUN chmod +x /usr/local/l3afd/start.sh 42 | RUN ln -s /usr/local/l3afd/v2.1.0/l3afd/l3afd /usr/local/l3afd/latest/l3afd 43 | RUN ln -s /usr/local/l3afd/v2.1.0/l3afd/l3afd.cfg /usr/local/l3afd/latest/l3afd.cfg 44 | 45 | ENTRYPOINT ["/bin/bash","/usr/local/l3afd/start.sh"] 46 | -------------------------------------------------------------------------------- /build-docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start 'l3afd' in the background 4 | /usr/local/l3afd/latest/l3afd --config /usr/local/l3afd/latest/l3afd.cfg & 5 | sleep 5; 6 | 7 | function check_and_wait() { 8 | sleep_time=$1 9 | while true; do 10 | if ps -p $(cat /var/run/l3afd.pid) > /dev/null; then 11 | sleep $sleep_time 12 | else 13 | break 14 | fi 15 | done 16 | } 17 | 18 | function on_term { 19 | echo "Signal $1 received" 20 | kill -SIGTERM $(cat /var/run/l3afd.pid) 21 | check_and_wait 1 22 | echo "L3AFd process has terminated" 23 | } 24 | 25 | trap 'on_term SIGHUP' SIGHUP 26 | trap 'on_term SIGINT' SIGINT 27 | trap 'on_term SIGQUIT' SIGQUIT 28 | trap 'on_term SIGTERM' SIGTERM 29 | trap 'on_term SIGSTOP' SIGSTOP 30 | trap 'on_term SIGSEGV' SIGSEGV 31 | trap 'on_term SIGILL' SIGILL 32 | trap 'on_term SIGKILL' SIGKILL 33 | trap 'on_term SIGABRT' SIGABRT 34 | trap 'on_term SIGBUS' SIGBUS 35 | 36 | check_and_wait 5 37 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | register_admind.go 2 | bpfprogs/cdbhelpers.go 3 | bpfprogs/bpfCfgs.go 4 | -------------------------------------------------------------------------------- /config/config_loader.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package config 5 | 6 | import ( 7 | "encoding/csv" 8 | "net/url" 9 | "strings" 10 | "time" 11 | 12 | "github.com/robfig/config" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | const ( 17 | cfgFatalMsg = "Could not read %s value %q from group %q in config file" 18 | cfgOptionalMsg = "Using default value %v after failure to read group:%s; field:%s" 19 | ) 20 | 21 | //Config Shortcuts-------------------------------------------------------------- 22 | // Note: For all the LoadXXX functions, we now have an equivalent LoadOptionalXXX variant. 23 | // The LoadOptionalXXX variant will accept a default value as a parameter and if it fails to 24 | // read in a value from the cfg, will return the default value as opposed to terminating odnd. 25 | // For any new parameters that are not essential and/or have reasonable defaults - it will be 26 | // a good idea to use the LoadOptionalXXX function. 27 | 28 | // LoadConfigString gets the value (as a string) for a field belonging to a group. 29 | // If the group and field are present - it returns the value 30 | // If the group or field are absent - it aborts the process 31 | // Note: Values that are encrypted are decrypted using a global key 32 | func LoadConfigString(confReader *config.Config, group, field string) string { 33 | return LoadConfigStringEncKey(confReader, group, field) 34 | } 35 | 36 | // LoadOptionalConfigString gets the value (as a string) for a field belonging to a group. 37 | // If the group and field are present - it returns the value 38 | // If the group or field are absent - it returns the supplied default value 39 | // Note: Values that are encrypted are decrypted using a global key 40 | func LoadOptionalConfigString(confReader *config.Config, group, field, defaultValue string) string { 41 | return LoadOptionalConfigStringEncKey(confReader, group, field, defaultValue) 42 | } 43 | 44 | // LoadOptionalConfigStringEncKey is similar to LoadOptionalConfigString, except that it accepts an optional decryption key. 45 | func LoadOptionalConfigStringEncKey(confReader *config.Config, group, field, defaultValue string) string { 46 | val, err := loadConfigStringEncKey(confReader, group, field) 47 | if err != nil { 48 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 49 | return defaultValue 50 | } 51 | return val 52 | } 53 | 54 | // LoadConfigStringEncKey is similar to LoadConfigString, except that it accepts an optional decryption key. 55 | func LoadConfigStringEncKey(confReader *config.Config, group, field string) string { 56 | val, err := loadConfigStringEncKey(confReader, group, field) 57 | if err != nil { 58 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "text", field, group) 59 | } 60 | return val 61 | } 62 | 63 | // loadConfigStringEncKey attempts to read the value for a given group and field in a config object. 64 | // If the value is absent - an error is returned to the caller, who can then abort execution or return a default. 65 | // If the value is present - it is returned to the caller, and optionally decrypted if the value starts with `ENC:` 66 | // Note: Decryption is done with the supplied key. If nil - a global key is used for decryption. 67 | func loadConfigStringEncKey(confReader *config.Config, group, field string) (string, error) { 68 | //Read value from config reader 69 | value, err := confReader.String(group, field) 70 | if err != nil { 71 | return "", err 72 | } 73 | return value, nil 74 | } 75 | 76 | func LoadConfigBool(confReader *config.Config, group, field string) bool { 77 | value, err := confReader.Bool(group, field) 78 | if err != nil { 79 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "logical", field, group) 80 | } 81 | return value 82 | } 83 | 84 | func LoadOptionalConfigBool(confReader *config.Config, group, field string, defaultValue bool) bool { 85 | value, err := confReader.Bool(group, field) 86 | if err != nil { 87 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 88 | return defaultValue 89 | } 90 | return value 91 | } 92 | 93 | func LoadConfigInt(confReader *config.Config, group, field string) int { 94 | value, err := confReader.Int(group, field) 95 | if err != nil { 96 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "integer", field, group) 97 | } 98 | return value 99 | } 100 | 101 | func LoadOptionalConfigInt(confReader *config.Config, group, field string, defaultValue int) int { 102 | value, err := confReader.Int(group, field) 103 | if err != nil { 104 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 105 | return defaultValue 106 | } 107 | return value 108 | } 109 | 110 | func LoadConfigFloat(confReader *config.Config, group, field string) float64 { 111 | value, err := confReader.Float(group, field) 112 | if err != nil { 113 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "decimal point", field, group) 114 | } 115 | return value 116 | } 117 | 118 | func LoadOptionalConfigFloat(confReader *config.Config, group, field string, defaultValue float64) float64 { 119 | value, err := confReader.Float(group, field) 120 | if err != nil { 121 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 122 | return defaultValue 123 | } 124 | return value 125 | } 126 | 127 | func LoadConfigDuration(confReader *config.Config, group, field string) time.Duration { 128 | value, err := time.ParseDuration(LoadConfigString(confReader, group, field)) 129 | if err != nil { 130 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "duration", field, group) 131 | } 132 | return value 133 | } 134 | 135 | func LoadOptionalConfigDuration(confReader *config.Config, group, field string, defaultValue time.Duration) time.Duration { 136 | stringValue := LoadOptionalConfigString(confReader, group, field, "") 137 | if len(stringValue) == 0 { 138 | return defaultValue 139 | } 140 | 141 | value, err := time.ParseDuration(stringValue) 142 | if err != nil { 143 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 144 | return defaultValue 145 | } 146 | return value 147 | } 148 | 149 | func LoadConfigURL(confReader *config.Config, group, field string) *url.URL { 150 | value, err := url.Parse(LoadConfigString(confReader, group, field)) 151 | if err != nil { 152 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "url", field, group) 153 | } 154 | return value 155 | } 156 | 157 | func LoadOptionalConfigURL(confReader *config.Config, group, field string, defaultValue *url.URL) *url.URL { 158 | stringValue := LoadOptionalConfigString(confReader, group, field, "") 159 | if len(stringValue) == 0 { 160 | return defaultValue 161 | } 162 | 163 | value, err := url.Parse(stringValue) 164 | if err != nil { 165 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 166 | return defaultValue 167 | } 168 | return value 169 | } 170 | 171 | // LoadConfigStringCSV splits a CSV config string value and returns the 172 | // resulting slice of strings. An emptyDefault []string is returned if the config 173 | // field is emptyDefault (as opposed to []string{""}, which strings.Split() would 174 | // return). 175 | func LoadConfigStringCSV(confReader *config.Config, group, field string) []string { 176 | CSVStr := strings.TrimSpace(LoadConfigString(confReader, group, field)) 177 | if CSVStr == "" { 178 | return []string{} 179 | } 180 | vals, err := csv.NewReader(strings.NewReader(CSVStr)).Read() 181 | if err != nil { 182 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "CSV string", field, group) 183 | } 184 | return vals 185 | } 186 | 187 | func LoadOptionalConfigStringCSV(confReader *config.Config, group, field string, defaultValue []string) []string { 188 | val, err := loadConfigStringEncKey(confReader, group, field) 189 | if err != nil { 190 | log.Info().Err(err).Msgf(cfgOptionalMsg, defaultValue, group, field) 191 | return defaultValue 192 | } 193 | CSVStr := strings.TrimSpace(val) 194 | if CSVStr == "" { 195 | return []string{} 196 | } 197 | vals, err := csv.NewReader(strings.NewReader(CSVStr)).Read() 198 | if err != nil { 199 | log.Fatal().Err(err).Msgf(cfgFatalMsg, "CSV string", field, group) 200 | } 201 | return vals 202 | } 203 | -------------------------------------------------------------------------------- /config/l3afd.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | 3 | [l3afd] 4 | pid-file: /var/run/l3afd.pid 5 | datacenter: dummy 6 | bpf-dir: /dev/shm 7 | bpf-log-dir: 8 | kernel-major-version: 5 9 | kernel-minor-version: 15 10 | shutdown-timeout: 25s 11 | http-client-timeout: 10s 12 | max-ebpf-restart-count: 3 13 | bpf-chaining-enabled: true 14 | swagger-api-enabled: true 15 | environment: DEV 16 | BpfMapDefaultPath: /sys/fs/bpf 17 | #file-log-location: /var/log/l3afd.log 18 | #file-log-max-size: 100 19 | #file-log-max-backups: 20 20 | #file-log-max-age: 60 21 | #json-format-logs: true 22 | 23 | [ebpf-repo] 24 | url: file:///srv/l3afd 25 | 26 | 27 | [web] 28 | metrics-addr: localhost:8898 29 | ebpf-poll-interval: 30s 30 | n-metric-samples: 20 31 | 32 | [xdp-root] 33 | package-name: xdp-root 34 | artifact: l3af_xdp_root.tar.gz 35 | command: xdp_root 36 | ingress-map-name: xdp_root_array 37 | version: latest 38 | object-file: xdp_root.bpf.o 39 | entry-function-name: xdp_root 40 | 41 | [tc-root] 42 | package-name: tc-root 43 | artifact: l3af_tc_root.tar.gz 44 | ingress-map-name: tc_ingress_root_array 45 | egress-map-name: tc_egress_root_array 46 | command: tc_root 47 | version: latest 48 | ingress-object-file: tc_root_ingress.bpf.o 49 | egress-object-file: tc_root_egress.bpf.o 50 | ingress-entry-function-name: tc_ingress_root 51 | egress-entry-function-name: tc_egress_root 52 | 53 | [ebpf-chain-debug] 54 | addr: localhost:8899 55 | enabled: true 56 | 57 | [l3af-configs] 58 | restapi-addr: localhost:7080 59 | 60 | [l3af-config-store] 61 | filename: /var/l3afd/l3af-config.json 62 | 63 | [mtls] 64 | enabled: false 65 | # TLS_1_2 or TLS_1_3 66 | # min-tls-version: TLS_1_3 67 | # cert-dir: /etc/l3af/certs 68 | # cacert-filename: ca.pem 69 | # server-crt-filename: server.crt 70 | # server-key-filename: server.key 71 | # how many days before expiry you want warning 72 | # cert-expiry-warning-days: 30 73 | # multiple domains seperated by comma 74 | # literal and regex are validated in lowercase 75 | # san-match-rules: .+l3afd.l3af.io,.*l3af.l3af.io,^l3afd.l3af.io$ 76 | 77 | [l3af-config-store] 78 | filename: /var/l3afd/l3af-config.json 79 | 80 | [graceful-restart] 81 | restart-artifacts-url: file:///srv/l3afd 82 | time-to-restart: 7 83 | basepath: /usr/local/l3afd 84 | version-limit: 100 85 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Some Ways to Contribute 4 | - [Getting started with L3AF](#getting-started-with-l3af) 5 | - [Report potential bugs](#report-potential-bugs) 6 | - [New features or product enhancements](#new-features-or-product-enhancements) 7 | - [Submitting a patch that fixes a bug](#submitting-a-patch-that-fixes-a-bug) 8 | - [Coding style](#coding-style) 9 | - [Guidelines for pull requests](#guidelines-for-pull-requests) 10 | - [Improve our guides and documentation](#improve-our-guides-and-documentation) 11 | - [Increase our test coverage](#increase-our-test-coverage) 12 | 13 | ### Getting started with L3AF 14 | 15 | See [L3AF](https://wiki.lfnetworking.org/display/L3AF/Getting+Started+with+L3AF) 16 | 17 | ### Report potential bugs 18 | 19 | First, **ensure the bug was not already reported** by searching on GitHub under 20 | [Issues](https://github.com/l3af-project/l3afd/issues). 21 | 22 | If you did not find a related bug, you can help us by 23 | [submitting a GitHub Issue](https://github.com/l3af-project/l3afd/issues/new). 24 | A good bug report provides a detailed description of the issue and step-by-step instructions 25 | for reliably reproducing the issue. 26 | 27 | We will aim to triage issues in [weekly TSC meetings](https://wiki.lfnetworking.org/display/L3AF/Community+Meetings). 28 | In case we are unable to repro the issue, we will request more information from you. There will be a waiting period of 29 | 2 weeks for the requested information and if there is no response, the issue will be closed. If this happens, 30 | please reopen the issue if you do get a repro and provide the requested information. 31 | 32 | If you found a security issue, please do not open a GitHub Issue, and instead [email](mailto:security@lists.l3af.io) it in detail. 33 | 34 | ### New features or product enhancements 35 | 36 | You can request or implement a new feature by [submitting a GitHub Issue](https://github.com/l3af-project/l3afd/issues/new). 37 | and communicate your proposal so that the L3AF community can review and provide feedback. Getting 38 | early feedback will help ensure your implementation work is accepted by the community. 39 | This will also allow us to better coordinate our efforts and minimize duplicated effort. 40 | 41 | ### Submitting a patch that fixes a bug 42 | 43 | Fork the repo and make your changes. Then open a new GitHub pull request with the patch. 44 | 45 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number 46 | if applicable. 47 | 48 | ### Guidelines for pull requests 49 | 50 | Great, you want to directly contribute to the l3af project and submit a pull request. 51 | It is recommended prior to working on a PR to submit an issue in github for the change you want 52 | to make describing the change and context around it. This gives the l3af maintainers a chance to review 53 | the issue and provide feedback and work with you on the change. If you have any questions, please 54 | feel free to reach out to the l3af maintainers via [Slack](https://app.slack.com/client/T02GD9YQJUT/C02GRTC0SAD) or 55 | [mail](main@lists.l3af.io) group. Below are some general guidelines to help ensure a successful PR approval. 56 | 57 | - Provide background why you are making the change and the issue it addresses 58 | - List what is changing and provide a high-level summary of the change 59 | - List any relevant dependencies linked to this change 60 | - Describe the tests that you ran to verify your changes and add/update test cases 61 | - Update relevant docs, especially if you've changed APIs 62 | - Ensure all GitHub CI/CD checks pass 63 | 64 | ### Coding Style 65 | 66 | See [uber-go](https://github.com/uber-go/guide/blob/master/style.md) 67 | 68 | ### Improve our guides and documentation 69 | 70 | We look forward to contributions improving our guides and documentation. 71 | Documentation should be written in an inclusive style. The [Google developer documentation](https://developers.google.com/style/inclusive-documentation) 72 | contains an excellent reference on this topic. 73 | 74 | ### Increase our test coverage 75 | 76 | Increase the code coverage by adding tests. PRs are expected to have 100% test coverage for added code. This can be 77 | verified with a coverage build. If your PR cannot have 100% coverage for some reason please clearly explain why when 78 | you open it. Run your tests and get coverage locally 79 | 80 | ```bash 81 | go test -race -covermode=atomic -coverprofile=coverage.out 82 | ``` 83 | 84 | ## Developer Certificate of Origin (DCO) 85 | 86 | The [Developer Certificate of Origin](https://developercertificate.org/) is a lightweight way for contributors 87 | to certify that they wrote or otherwise have the right to submit the code they are contributing to the project. 88 | This App enforces the Developer Certificate of Origin on Pull Requests. It requires all commit messages to contain 89 | the ```Signed-off-by``` line with an email address that matches the commit author. 90 | 91 | ## Governance 92 | 93 | Please refer to the [governance repo](https://github.com/l3af-project/governance) for Project Charter, Code of Conduct, 94 | Release Process, and Committee Members. 95 | -------------------------------------------------------------------------------- /docs/configdoc.md: -------------------------------------------------------------------------------- 1 | # L3AFD Config Options Documentation 2 | 3 | See [l3afd.cfg](https://github.com/l3af-project/l3afd/blob/main/config/l3afd.cfg) for a full example configuration. 4 | 5 | 6 | ``` 7 | [DEFAULT] 8 | 9 | [l3afd] 10 | pid-file: ./l3afd.pid 11 | datacenter: dc 12 | bpf-dir: /dev/shm 13 | bpf-log-dir: 14 | shutdown-timeout: 1s 15 | http-client-timeout: 10s 16 | max-ebpf-restart-count: 3 17 | bpf-chaining-enabled: true 18 | swagger-api-enabled: false 19 | # PROD | DEV 20 | environment: PROD 21 | .... 22 | ``` 23 | 24 | ### Below is the detailed documentation for each field 25 | 26 | 27 | ## [l3afd] 28 | 29 | | FieldName | Default | Description | Required | 30 | | ------------- |--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| --------------- | 31 | |pid-file| `"/var/l3afd/l3afd.pid"` | The path to the l3afd.pid file which contains process id of L3afd | Yes | 32 | |datacenter| `"dc"` | Name of Datacenter | Yes | 33 | |bpf-dir| `"/dev/shm"` | Absolute Path where eBPF packages are to be extracted | Yes | 34 | |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 | 35 | |kernel-major-version| `"5"` | Major version of the kernel required to run eBPF programs (Linux Only) | No | 36 | |kernel-minor-version| `"1"` | Minor version of the kernel required to run eBPF programs (Linux Only) | No | 37 | |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 | 38 | |http-client-timeout| `"10s"` | Maximum amount of time allowed to get HTTP response headers when fetching a package from a repository | No | 39 | |max-nf-restart-count| `"3"` | Maximum number of tries to restart eBPF applications if they are not running | No | 40 | |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 | 41 | |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 | 42 | |environment| `"PROD"` | If set to anything other than "PROD", mTLS security will not be checked | Yes | 43 | |BpfMapDefaultPath| `"/sys/fs/bpf"` | The base pin path for eBPF maps | Yes | 44 | | file-log-location | `"/var/log/l3afd.log"` | Location of the log file | No | 45 | | file-log-max-size | `"100"` | Max size in megabytes for Log file rotation | No | 46 | | file-log-max-backups | `"20"` | Max size in megabytes for Log file rotation | No | 47 | | file-log-max-age | `"60"` | Max number of days to keep Log files | No | 48 | | json-format-logs | `"false"` | Logs to be printed as a JSON object | No | 49 | 50 | ## [ebpf-repo] 51 | | FieldName | Default | Description | Required | 52 | | ------------- |----------------------------| --------------- |----------| 53 | |url| `"file:///var/l3afd/repo"` |Default repository from which to download eBPF packages| Yes | 54 | 55 | ## [web] 56 | 57 | | FieldName | Default | Description | Required | 58 | |--------------------| ------------- | --------------- |----------| 59 | | 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 | 60 | | ebpf-poll-interval |`"30s"`|Periodic interval at which to scrape metrics using Prometheus| No | 61 | | n-metric-samples |`"20"`|Number of Metric Samples| No | 62 | 63 | 64 | ## [xdp-root] 65 | This section is needed when bpf-chaining-enabled is set to true. 66 | 67 | | FieldName | Default | Description | Required | 68 | |---------------------|--------------------------|--------------------------------------------------------------------------| --------------- | 69 | | package-name | `"xdp-root"` | Name of subdirectory in which to extract artifact | Yes | 70 | | artifact | `"l3af_xdp_root.tar.gz"` | Filename of xdp-root package. Only tar.gz and .zip formats are supported | Yes | 71 | | ingress-map-name | `"xdp_root_array"` | Ingress map name of xdp-root program | Yes | 72 | | command | `"xdp_root"` | Command to run xdp-root program | Yes | 73 | | version | `"latest"` | Version of xdp-root program | Yes | 74 | | object-file | `"xdp_root.bpf.o"` | File containing the object code for xdp-root program | Yes | 75 | | entry-function-name | `"xdp_root"` | Name of the function that begins the XDP-root program | Yes | 76 | 77 | 78 | ## [tc-root] 79 | This section is needed when bpf-chaining-enabled is set to true. 80 | 81 | | FieldName | Default | Description | Required | 82 | |-----------------------------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| --------------- | 83 | | pakage-name | `"tc-root"` | Name of subdirectory in which to extract artifact | Yes | 84 | | artifact | `"l3af_tc_root.tar.gz"` | Filename of tc_root package | Yes | 85 | | ingress-map-name | `"tc_ingress_root_array"` | Ingress map name of tc_root program | Yes | 86 | | 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 | 87 | | command | `"tc_root"` | Command to run tc_root program | Yes | 88 | | version | `"latest"` | Version of tc_root program | Yes | 89 | | ingress-object-file | `"tc_root_ingress.bpf.o"` | File containing the object code for tc-root ingress program | Yes | 90 | | egress-object-file | `"tc_root_egress.bpf.o"` | File containing the object code for tc-root egress program | Yes | 91 | | ingress-entry-function-name | `"tc_ingress_root"` | Name of the function that begins the tc-root ingress program | Yes | 92 | | egress-entry-function-name | `"tc_egress_root"` | Name of the function that begins the tc-root egress program | Yes | 93 | 94 | 95 | ## [ebpf-chain-debug] 96 | | FieldName | Default | Description | Required | 97 | |-----------|--------------------|----------------------------------------------------------------|----------| 98 | | addr | `"localhost:8899"` | Hostname and Port of chaining debug REST API | No | 99 | | enabled | `"false"` | Boolean to check ebpf chaining debug details is enabled or not | No | 100 | 101 | ## [l3af-configs] 102 | | FieldName | Default | Description | Required | 103 | | ------------- | ------------- | --------------- |----------| 104 | |restapi-addr|`"localhost:53000"`| Hostname and Port of l3af-configs REST API | No | 105 | 106 | ## [l3af-config-store] 107 | | FieldName | Default | Description | Required | 108 | | ------------- | ------------- | --------------- | --------------- | 109 | |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 | 110 | 111 | ## [mtls] 112 | | FieldName | Default | Description | Required | 113 | | ------------- |------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| 114 | |enabled| `"true"` | Boolean controlling whether mTLS is enabled or not on the REST API exposed by l3afd | No | 115 | |min-tls-version| `"1.3"` | Minimum tls version allowed | No | 116 | |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 | 117 | |server-crt-filename| `"server.crt"` | Server's ca certificate filename | No | 118 | |server-key-filename| `"server.key"` | Server's mtls key filename | No | 119 | |cert-expiry-warning-days| `"30"` | How many days before expiry you want warning | No | 120 | |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 | 121 | -------------------------------------------------------------------------------- /docs/graceful-restart-guide.md: -------------------------------------------------------------------------------- 1 | # Guide to L3AFD Graceful Restart 2 | 3 | ## Prerequisites 4 | 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/`. 5 | 6 | ## Directory Structure 7 | 8 | Firstly, create a directory structure as shown below: 9 | 10 | ``` 11 | /usr/local/l3afd# tree 12 | . 13 | ├── latest 14 | │ ├── l3afd -> /usr/local/l3afd/v2.0.0/l3afd/l3afd 15 | │ └── l3afd.cfg -> /usr/local/l3afd/v2.0.0/l3afd/l3afd.cfg 16 | ├── start.sh 17 | └── v2.0.0 18 | └── l3afd 19 | ├── l3afd 20 | └── l3afd.cfg 21 | ``` 22 | L3afd runs a certain version (in this case, v2.0.0), and in the 'latest' folder, it is symlinked. 23 | 24 | ## Starting the l3afd Service 25 | To start the service, run the start.sh script: 26 | 27 | ``` 28 | /usr/local/l3afd# cat start.sh 29 | #!/bin/bash 30 | /usr/local/l3afd/latest/l3afd --config /usr/local/l3afd/latest/l3afd.cfg & 31 | ``` 32 | 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. 33 | 34 | ## Upgrading to a New Version 35 | 36 | 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. 37 | 38 | Here's an example of how to create an artifact: 39 | 40 | 1. Create a folder at /srv/l3afd/pkg/. 41 | 2. Inside that, create /srv/l3afd/pkg/l3afd. 42 | 3. Copy your v2.x.0 binary and cfg file to the above folder. 43 | 4. Create a tar gunzip file: `tar -czvf l3afd.tar.gz l3afd` or create a zip file. 44 | 45 | The artifact can be served from a remote server, local server, or local folder. 46 | 47 | For a local start, your payload for the restart would look like this: 48 | 49 | ``` 50 | { 51 | "hostname": "l3af-test-host", 52 | "version": "v2.x.x", 53 | } 54 | ``` 55 | ## Restarting the Service 56 | 57 | To restart the service, use the following API call: 58 | 59 | ``` 60 | curl -X PUT http://localhost:7080/l3af/configs/v1/restart -d "@restart.json" 61 | ``` 62 | During this graceful restart, eBPF Programs of type probes and all user programs are restarted. 63 | 64 | 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. -------------------------------------------------------------------------------- /docs/prod-deploy-guide.md: -------------------------------------------------------------------------------- 1 | # Guide to use L3AF in production environments 2 | 3 | ## Installing l3afd 4 | 5 | Download the latest build artifacts for the last stable release on the l3afd [repo page](../../../) 6 | 7 | ## Configuring l3afd 8 | 9 | This guide lists recommendations on how to run l3afd in a production environment. Please see [l3afd.cfg](../config/l3afd.cfg) for a sample configuration. 10 | 11 | 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. 12 | 13 | * Make sure `environment: PROD` is set to prevent l3afd starting up in an insecure configuration. 14 | 15 | * Ensure mTLS is set to `enabled: true` in the configuration. 16 | 17 | * It is recommended to use TLS version `1.3`. 18 | 19 | * Do not use self-signed certificates. It is always encouraged to use well-known root certificates to create server certificates and client certificates. 20 | 21 | * 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. 22 | 23 | * 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. 24 | * eBPF repository artifacts are retrieved by joining the following elements to build the complete path: `https://////` or `file:///////`. 25 | 26 | ## Running l3afd 27 | 28 | * 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. 29 | 30 | * l3afd only supports handling the following signals `SIGINT`, `SIGTERM`, which will cause l3afd to perform a clean shut down. 31 | 32 | * l3afd can be configured through a system manager to start on boot, such as systemd. 33 | -------------------------------------------------------------------------------- /docs/swagger.md: -------------------------------------------------------------------------------- 1 | ## Swaggo setup 2 | 3 | Our first task is to install the libraries we are dependent on. Run the following commands from the commandline: 4 | ``` 5 | go get -u github.com/swaggo/swag/cmd/swag 6 | go get -u github.com/swaggo/http-swagger 7 | go get -u github.com/alecthomas/template 8 | 9 | ``` 10 | The first two commands install swag and http-swagger respectively: 11 | 12 | #### swag 13 | 14 | This library converts Go annotations to Swagger 2.0 docs (swagger.json/swagger.yaml), which are later used by 15 | http-swagger to serve the Swagger UI. 16 | 17 | #### http-swagger 18 | 19 | This library helps to serve the Swagger UI using the docs generated by swag. 20 | 21 | The third command is to install template, a fork of Go’s text/template package. This dependency is required in the docs.go 22 | file generated by swag, and we’ll see an error while running the application without it. 23 | 24 | ## Generate Swagger documentation 25 | 26 | #### Adding annotations in code 27 | 28 | If you have not added the annotations, follow these docs 29 | 30 | * https://www.soberkoder.com/swagger-go-api-swaggo/ 31 | * https://github.com/swaggo/swag#declarative-comments-format 32 | 33 | #### Generating Swagger specs (swagger.json and swagger.yaml) 34 | 35 | From the root folder of `l3afd` run following command 36 | ``` 37 | swag init -d "./" -g "apis/configwatch.go" 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | models.BPFProgram: 4 | properties: 5 | admin_status: 6 | description: Program admin status enabled or disabled 7 | type: string 8 | artifact: 9 | description: Artifact file name 10 | type: string 11 | cfg_version: 12 | description: Config version 13 | type: integer 14 | cmd_config: 15 | description: Program config providing command 16 | type: string 17 | cmd_start: 18 | description: Program start command 19 | type: string 20 | cmd_status: 21 | description: Program status command 22 | type: string 23 | cmd_stop: 24 | description: Program stop command 25 | type: string 26 | cmd_update: 27 | description: Program update config command 28 | type: string 29 | config_args: 30 | allOf: 31 | - $ref: '#/definitions/models.L3afDNFArgs' 32 | description: Map of arguments to config command 33 | config_file_path: 34 | description: Config file location 35 | type: string 36 | cpu: 37 | description: User program cpu limits 38 | type: integer 39 | ebpf_package_repo_url: 40 | description: Download url for Program 41 | type: string 42 | entry_function_name: 43 | description: BPF entry function name to load 44 | type: string 45 | id: 46 | description: Program id 47 | type: integer 48 | is_plugin: 49 | description: User program is plugin or not 50 | type: boolean 51 | map_args: 52 | description: Config BPF Map of arguments 53 | items: 54 | $ref: '#/definitions/models.L3afDMapArg' 55 | type: array 56 | map_name: 57 | description: BPF map to store next program fd 58 | type: string 59 | memory: 60 | description: User program memory limits 61 | type: integer 62 | monitor_maps: 63 | description: Metrics BPF maps 64 | items: 65 | $ref: '#/definitions/models.L3afDNFMetricsMap' 66 | type: array 67 | name: 68 | description: Name of the BPF program package 69 | type: string 70 | object_file: 71 | description: Object file contains BPF code 72 | type: string 73 | prog_type: 74 | description: Program type XDP or TC 75 | type: string 76 | rules: 77 | description: Config rules 78 | type: string 79 | rules_file: 80 | description: Config rules file name 81 | type: string 82 | seq_id: 83 | description: Sequence position in the chain 84 | type: integer 85 | start_args: 86 | allOf: 87 | - $ref: '#/definitions/models.L3afDNFArgs' 88 | description: Map of arguments to start command 89 | status_args: 90 | allOf: 91 | - $ref: '#/definitions/models.L3afDNFArgs' 92 | description: Map of arguments to status command 93 | stop_args: 94 | allOf: 95 | - $ref: '#/definitions/models.L3afDNFArgs' 96 | description: Map of arguments to stop command 97 | update_args: 98 | allOf: 99 | - $ref: '#/definitions/models.L3afDNFArgs' 100 | description: Map of arguments to update command 101 | user_program_daemon: 102 | description: User program daemon or not 103 | type: boolean 104 | version: 105 | description: Program version 106 | type: string 107 | type: object 108 | models.BPFProgramNames: 109 | properties: 110 | probes: 111 | description: names of the probe eBPF programs 112 | items: 113 | type: string 114 | type: array 115 | tc_egress: 116 | description: names of the TC egress eBPF programs 117 | items: 118 | type: string 119 | type: array 120 | tc_ingress: 121 | description: names of the TC ingress eBPF programs 122 | items: 123 | type: string 124 | type: array 125 | xdp_ingress: 126 | description: names of the XDP ingress eBPF programs 127 | items: 128 | type: string 129 | type: array 130 | type: object 131 | models.BPFPrograms: 132 | properties: 133 | probes: 134 | description: list of probe bpf programs 135 | items: 136 | $ref: '#/definitions/models.BPFProgram' 137 | type: array 138 | tc_egress: 139 | description: list of tc egress bpf programs 140 | items: 141 | $ref: '#/definitions/models.BPFProgram' 142 | type: array 143 | tc_ingress: 144 | description: list of tc ingress bpf programs 145 | items: 146 | $ref: '#/definitions/models.BPFProgram' 147 | type: array 148 | xdp_ingress: 149 | description: list of xdp ingress bpf programs 150 | items: 151 | $ref: '#/definitions/models.BPFProgram' 152 | type: array 153 | type: object 154 | models.KeyValue: 155 | properties: 156 | key: 157 | description: Key 158 | type: integer 159 | value: 160 | description: Value 161 | type: integer 162 | type: object 163 | models.L3afBPFProgramNames: 164 | properties: 165 | bpf_programs: 166 | allOf: 167 | - $ref: '#/definitions/models.BPFProgramNames' 168 | description: List of eBPF program names to remove 169 | host_name: 170 | description: Host name or pod name 171 | type: string 172 | iface: 173 | description: Interface name 174 | type: string 175 | type: object 176 | models.L3afBPFPrograms: 177 | properties: 178 | bpf_programs: 179 | allOf: 180 | - $ref: '#/definitions/models.BPFPrograms' 181 | description: List of bpf programs 182 | host_name: 183 | description: Host name or pod name 184 | type: string 185 | iface: 186 | description: Interface name 187 | type: string 188 | type: object 189 | models.L3afDMapArg: 190 | properties: 191 | args: 192 | description: BPF map arguments 193 | items: 194 | $ref: '#/definitions/models.KeyValue' 195 | type: array 196 | name: 197 | description: BPF map name 198 | type: string 199 | type: object 200 | models.L3afDNFArgs: 201 | additionalProperties: true 202 | type: object 203 | models.L3afDNFMetricsMap: 204 | properties: 205 | aggregator: 206 | description: Aggregation function names 207 | type: string 208 | key: 209 | description: Index of the bpf map 210 | type: integer 211 | name: 212 | description: BPF map name 213 | type: string 214 | type: object 215 | info: 216 | contact: {} 217 | description: Configuration APIs to deploy and get the details of the eBPF Programs 218 | on the node 219 | title: L3AFD APIs 220 | version: "1.0" 221 | paths: 222 | /l3af/configs/v1: 223 | get: 224 | consumes: 225 | - application/json 226 | description: Returns details of the configuration of eBPF Programs for all interfaces 227 | on a node 228 | produces: 229 | - application/json 230 | responses: 231 | "200": 232 | description: OK 233 | summary: Returns details of the configuration of eBPF Programs for all interfaces 234 | on a node 235 | /l3af/configs/v1/{iface}: 236 | get: 237 | consumes: 238 | - application/json 239 | description: Returns details of the configuration of eBPF Programs for a given 240 | interface 241 | parameters: 242 | - description: interface name 243 | in: path 244 | name: iface 245 | required: true 246 | type: string 247 | produces: 248 | - application/json 249 | responses: 250 | "200": 251 | description: OK 252 | summary: Returns details of the configuration of eBPF Programs for a given interface 253 | /l3af/configs/v1/add: 254 | post: 255 | consumes: 256 | - application/json 257 | description: Adds new eBPF Programs on node 258 | parameters: 259 | - description: BPF programs 260 | in: body 261 | name: cfgs 262 | required: true 263 | schema: 264 | items: 265 | $ref: '#/definitions/models.L3afBPFPrograms' 266 | type: array 267 | produces: 268 | - application/json 269 | responses: 270 | "200": 271 | description: OK 272 | summary: Adds new eBPF Programs on node 273 | /l3af/configs/v1/delete: 274 | post: 275 | consumes: 276 | - application/json 277 | description: Removes eBPF Programs on node 278 | parameters: 279 | - description: BPF program names 280 | in: body 281 | name: cfgs 282 | required: true 283 | schema: 284 | items: 285 | $ref: '#/definitions/models.L3afBPFProgramNames' 286 | type: array 287 | produces: 288 | - application/json 289 | responses: 290 | "200": 291 | description: OK 292 | summary: Removes eBPF Programs on node 293 | /l3af/configs/v1/restart: 294 | put: 295 | consumes: 296 | - application/json 297 | description: Store meta data about ebpf programs and exit 298 | parameters: 299 | - description: BPF programs 300 | in: body 301 | name: cfgs 302 | required: true 303 | schema: 304 | items: 305 | $ref: '#/definitions/models.L3afBPFPrograms' 306 | type: array 307 | produces: 308 | - application/json 309 | responses: 310 | "200": 311 | description: OK 312 | summary: Store meta data about ebpf programs and exit 313 | /l3af/configs/v1/update: 314 | post: 315 | consumes: 316 | - application/json 317 | description: Update eBPF Programs configuration 318 | parameters: 319 | - description: BPF programs 320 | in: body 321 | name: cfgs 322 | required: true 323 | schema: 324 | items: 325 | $ref: '#/definitions/models.L3afBPFPrograms' 326 | type: array 327 | produces: 328 | - application/json 329 | responses: 330 | "200": 331 | description: OK 332 | summary: Update eBPF Programs configuration 333 | swagger: "2.0" 334 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/l3af-project/l3afd/v2 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.20.0 7 | github.com/go-chi/chi/v5 v5.2.3 8 | github.com/mitchellh/go-ps v1.0.0 9 | github.com/prometheus/client_golang v1.23.2 10 | github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e 11 | github.com/rs/zerolog v1.33.0 12 | github.com/safchain/ethtool v0.7.0 13 | github.com/swaggo/http-swagger v1.3.4 14 | github.com/swaggo/swag v1.16.4 15 | golang.org/x/sys v0.38.0 // exclude 16 | ) 17 | 18 | require ( 19 | github.com/florianl/go-tc v0.4.7 20 | go.uber.org/mock v0.6.0 21 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 22 | ) 23 | 24 | require ( 25 | github.com/KyleBanks/depth v1.2.1 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/spec v0.20.9 // indirect 31 | github.com/go-openapi/swag v0.22.4 // indirect 32 | github.com/google/go-cmp v0.7.0 // indirect 33 | github.com/josharian/intern v1.0.0 // indirect 34 | github.com/josharian/native v1.1.0 // indirect 35 | github.com/mailru/easyjson v0.7.7 // indirect 36 | github.com/mattn/go-colorable v0.1.13 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/mdlayher/netlink v1.7.2 // indirect 39 | github.com/mdlayher/socket v0.5.0 // indirect 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 41 | github.com/prometheus/client_model v0.6.2 // indirect 42 | github.com/prometheus/common v0.66.1 // indirect 43 | github.com/prometheus/procfs v0.16.1 // indirect 44 | github.com/swaggo/files v1.0.1 // indirect 45 | go.yaml.in/yaml/v2 v2.4.2 // indirect 46 | golang.org/x/net v0.46.0 // indirect 47 | golang.org/x/sync v0.17.0 // indirect 48 | golang.org/x/tools v0.38.0 // indirect 49 | google.golang.org/protobuf v1.36.8 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/gob" 9 | "encoding/json" 10 | "errors" 11 | "flag" 12 | "fmt" 13 | "io" 14 | "net" 15 | "os" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "time" 21 | 22 | "gopkg.in/natefinch/lumberjack.v2" 23 | 24 | "github.com/l3af-project/l3afd/v2/apis" 25 | "github.com/l3af-project/l3afd/v2/apis/handlers" 26 | "github.com/l3af-project/l3afd/v2/bpfprogs" 27 | "github.com/l3af-project/l3afd/v2/config" 28 | "github.com/l3af-project/l3afd/v2/models" 29 | "github.com/l3af-project/l3afd/v2/pidfile" 30 | "github.com/l3af-project/l3afd/v2/restart" 31 | "github.com/l3af-project/l3afd/v2/stats" 32 | "github.com/l3af-project/l3afd/v2/utils" 33 | 34 | "github.com/rs/zerolog" 35 | "github.com/rs/zerolog/log" 36 | ) 37 | 38 | const daemonName = "l3afd" 39 | 40 | var stateSockPath string 41 | 42 | func setupLogging(conf *config.Config) { 43 | // ConsoleWriter formats the logs for user-readability 44 | if conf.JSONFormatLogs { 45 | log.Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() 46 | } 47 | 48 | // Set the default Log level 49 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 50 | log.Info().Msgf("Log level set to %q", zerolog.InfoLevel) 51 | 52 | if logLevelStr := os.Getenv("L3AF_LOG_LEVEL"); logLevelStr != "" { 53 | if logLevel, err := zerolog.ParseLevel(logLevelStr); err != nil { 54 | log.Error().Err(err).Msg("Invalid environment-specified log level. Defaulting to INFO.") 55 | } else { 56 | zerolog.SetGlobalLevel(logLevel) 57 | log.Info().Msgf("Log level set to %q via environment variable", logLevel) 58 | } 59 | } 60 | 61 | if conf.FileLogLocation != "" { 62 | log.Info().Msgf("Saving logs to file: %s", conf.FileLogLocation) 63 | saveLogsToFile(conf) 64 | return 65 | } 66 | 67 | } 68 | 69 | func saveLogsToFile(conf *config.Config) { 70 | logFileWithRotation := &lumberjack.Logger{ 71 | Filename: conf.FileLogLocation, 72 | MaxSize: conf.FileLogMaxSize, // Max size in megabytes 73 | MaxBackups: conf.FileLogMaxBackups, // Max number of old log files to keep 74 | MaxAge: conf.FileLogMaxAge, // Max number of days to keep log files 75 | } 76 | // Create a multi-writer for stdout and the file 77 | multiWriter := zerolog.MultiLevelWriter(os.Stdout, logFileWithRotation) 78 | 79 | if conf.JSONFormatLogs { 80 | log.Logger = log.Output(zerolog.ConsoleWriter{ 81 | Out: multiWriter}) 82 | 83 | } else { 84 | log.Logger = zerolog.New(multiWriter).With().Timestamp().Logger() 85 | } 86 | } 87 | 88 | func main() { 89 | models.CloseForRestart = make(chan struct{}) 90 | models.IsReadOnly = false 91 | models.CurrentWriteReq = 0 92 | models.StateLock = sync.Mutex{} 93 | ctx, cancel := context.WithCancel(context.Background()) 94 | defer cancel() 95 | 96 | //Default Logger (uses user-friendly colored log statements in RFC3339Nano (e.g., 2006-01-02T15:04:05.999999999Z07:00) format) 97 | zerolog.TimeFieldFormat = time.RFC3339Nano 98 | log.Logger = log.Output(zerolog.ConsoleWriter{ 99 | Out: os.Stderr}) 100 | 101 | log.Info().Msgf("%s started.", daemonName) 102 | 103 | var confPath string 104 | flag.StringVar(&confPath, "config", "config/l3afd.cfg", "config path") 105 | 106 | flag.Parse() 107 | initVersion() 108 | conf, err := config.ReadConfig(confPath) 109 | if err != nil { 110 | log.Fatal().Err(err).Msgf("Unable to parse config %q", confPath) 111 | } 112 | 113 | // Setup logging according to the configuration provided 114 | setupLogging(conf) 115 | populateVersions(conf) 116 | if err = pidfile.CheckPIDConflict(conf.PIDFilename); err != nil { 117 | if err = setupForRestartOuter(ctx, conf); err != nil { 118 | log.Warn().Msg("Doing Normal Startup") 119 | } else { 120 | log.Fatal().Err(err).Msgf("The PID file: %s, is in an unacceptable state", conf.PIDFilename) 121 | } 122 | } 123 | if err = pidfile.CreatePID(conf.PIDFilename); err != nil { 124 | log.Fatal().Err(err).Msgf("The PID file: %s, could not be created", conf.PIDFilename) 125 | } 126 | 127 | if runtime.GOOS == "linux" { 128 | if err = checkKernelVersion(conf); err != nil { 129 | log.Fatal().Err(err).Msg("The unsupported kernel version please upgrade") 130 | } 131 | } 132 | 133 | if err = registerL3afD(conf); err != nil { 134 | log.Error().Err(err).Msg("L3afd registration failed") 135 | } 136 | 137 | ebpfConfigs, err := SetupNFConfigs(ctx, conf) 138 | if err != nil { 139 | log.Fatal().Err(err).Msg("L3afd failed to start") 140 | } 141 | 142 | t, err := ReadConfigsFromConfigStore(conf) 143 | if err != nil { 144 | log.Error().Err(err).Msg("L3afd failed to read configs from store") 145 | } 146 | 147 | if t != nil { 148 | if err := ebpfConfigs.DeployeBPFPrograms(t); err != nil { 149 | log.Error().Err(err).Msg("L3afd failed to deploy persistent configs from store") 150 | } 151 | } 152 | 153 | if err := handlers.InitConfigs(ebpfConfigs); err != nil { 154 | log.Fatal().Err(err).Msg("L3afd failed to initialise configs") 155 | } 156 | 157 | if conf.EBPFChainDebugEnabled { 158 | bpfprogs.SetupBPFDebug(conf.EBPFChainDebugAddr, ebpfConfigs) 159 | } 160 | <-models.CloseForRestart 161 | os.Exit(0) 162 | } 163 | 164 | func SetupNFConfigs(ctx context.Context, conf *config.Config) (*bpfprogs.NFConfigs, error) { 165 | // Get Hostname 166 | machineHostname, err := os.Hostname() 167 | if err != nil { 168 | log.Error().Err(err).Msg("Could not get hostname from OS") 169 | } 170 | // setup Metrics endpoint 171 | stats.SetupMetrics(machineHostname, daemonName, conf.MetricsAddr) 172 | 173 | pMon := bpfprogs.NewPCheck(conf.MaxEBPFReStartCount, conf.BpfChainingEnabled, conf.EBPFPollInterval) 174 | bpfM := bpfprogs.NewpBpfMetrics(conf.BpfChainingEnabled, conf.NMetricSamples) 175 | nfConfigs, err := bpfprogs.NewNFConfigs(ctx, machineHostname, conf, pMon, bpfM) 176 | if err != nil { 177 | return nil, fmt.Errorf("error in NewNFConfigs setup: %v", err) 178 | } 179 | 180 | if err := apis.StartConfigWatcher(ctx, machineHostname, daemonName, conf, nfConfigs); err != nil { 181 | return nil, fmt.Errorf("error in version announcer: %v", err) 182 | } 183 | return nfConfigs, nil 184 | } 185 | 186 | func checkKernelVersion(conf *config.Config) error { 187 | const minVerLen = 2 188 | 189 | kernelVersion, err := utils.GetKernelVersion() 190 | if err != nil { 191 | return fmt.Errorf("failed to find kernel version: %v", err) 192 | } 193 | 194 | //validate version 195 | ver := strings.Split(kernelVersion, ".") 196 | if len(ver) < minVerLen { 197 | return fmt.Errorf("expected minimum kernel version length %d and got %d, ver %+q", minVerLen, len(ver), ver) 198 | } 199 | major_ver, err := strconv.Atoi(ver[0]) 200 | if err != nil { 201 | return fmt.Errorf("failed to find kernel major version: %v", err) 202 | } 203 | minor_ver, err := strconv.Atoi(ver[1]) 204 | if err != nil { 205 | return fmt.Errorf("failed to find kernel minor version: %v", err) 206 | } 207 | 208 | if major_ver > conf.MinKernelMajorVer { 209 | return nil 210 | } 211 | if major_ver == conf.MinKernelMajorVer && minor_ver >= conf.MinKernelMinorVer { 212 | return nil 213 | } 214 | 215 | return fmt.Errorf("expected Kernel version >= %d.%d", conf.MinKernelMajorVer, conf.MinKernelMinorVer) 216 | } 217 | 218 | func ReadConfigsFromConfigStore(conf *config.Config) ([]models.L3afBPFPrograms, error) { 219 | 220 | // check for persistent file 221 | if _, err := os.Stat(conf.L3afConfigStoreFileName); errors.Is(err, os.ErrNotExist) { 222 | log.Warn().Msgf("no persistent config exists") 223 | return nil, nil 224 | } 225 | 226 | file, err := os.OpenFile(conf.L3afConfigStoreFileName, os.O_RDONLY, os.ModePerm) 227 | defer func() { 228 | _ = file.Close() 229 | }() 230 | 231 | if err != nil { 232 | return nil, fmt.Errorf("failed to open persistent file (%s): %v", conf.L3afConfigStoreFileName, err) 233 | } 234 | 235 | byteValue, err := io.ReadAll(file) 236 | if err != nil { 237 | return nil, fmt.Errorf("failed to read persistent file (%s): %v", conf.L3afConfigStoreFileName, err) 238 | } 239 | 240 | var t []models.L3afBPFPrograms 241 | if err = json.Unmarshal(byteValue, &t); err != nil { 242 | return nil, fmt.Errorf("failed to unmarshal persistent config json: %v", err) 243 | } 244 | return t, nil 245 | } 246 | 247 | // setupForRestartOuter is a wrapper for setupForRestart, written for better error handling 248 | func setupForRestartOuter(ctx context.Context, conf *config.Config) error { 249 | if _, err := os.Stat(models.HostSock); os.IsNotExist(err) { 250 | return err 251 | } 252 | stateSockPath = models.StateSock 253 | models.IsReadOnly = true 254 | err := setupForRestart(ctx, conf) 255 | if err != nil { 256 | sendState("Failed") 257 | log.Fatal().Err(err).Msg("unable to restart the l3afd") 258 | } 259 | sendState("Ready") 260 | models.IsReadOnly = false 261 | <-models.CloseForRestart 262 | os.Exit(0) 263 | return nil 264 | } 265 | 266 | // setupForRestart will start the l3afd with state provided by other l3afd instance 267 | func setupForRestart(ctx context.Context, conf *config.Config) error { 268 | conn, err := net.Dial("unix", models.HostSock) 269 | if err != nil { 270 | return fmt.Errorf("unable to dial unix domain socket : %w", err) 271 | } 272 | decoder := gob.NewDecoder(conn) 273 | var t models.L3AFALLHOSTDATA 274 | err = decoder.Decode(&t) 275 | if err != nil { 276 | conn.Close() 277 | return fmt.Errorf("unable to decode") 278 | } 279 | conn.Close() 280 | machineHostname, err := os.Hostname() 281 | if err != nil { 282 | return fmt.Errorf("unable to fetch the hostname") 283 | } 284 | 285 | l, err := restart.GetNetListener(3, "stat_server") 286 | if err != nil { 287 | return fmt.Errorf("getting stat_server listener failed") 288 | } 289 | models.AllNetListeners.Store("stat_http", l) 290 | 291 | l, err = restart.GetNetListener(4, "main_server") 292 | if err != nil { 293 | return fmt.Errorf("getting main_server listener failed") 294 | } 295 | models.AllNetListeners.Store("main_http", l) 296 | 297 | if conf.EBPFChainDebugEnabled { 298 | l, err = restart.GetNetListener(5, "debug_server") 299 | if err != nil { 300 | return fmt.Errorf("getting main_server listener failed") 301 | } 302 | models.AllNetListeners.Store("debug_http", l) 303 | } 304 | // setup Metrics endpoint 305 | stats.SetupMetrics(machineHostname, daemonName, conf.MetricsAddr) 306 | restart.SetMetrics(t) 307 | pMon := bpfprogs.NewPCheck(conf.MaxEBPFReStartCount, conf.BpfChainingEnabled, conf.EBPFPollInterval) 308 | bpfM := bpfprogs.NewpBpfMetrics(conf.BpfChainingEnabled, conf.NMetricSamples) 309 | log.Info().Msgf("Restoring Previous State Graceful Restart") 310 | ebpfConfigs, err := restart.Convert(ctx, t, conf) 311 | ebpfConfigs.BpfMetricsMon = bpfM 312 | ebpfConfigs.ProcessMon = pMon 313 | if err != nil { 314 | return fmt.Errorf("failed to convert deserilaze the state") 315 | } 316 | err = ebpfConfigs.StartAllUserProgramsAndProbes() 317 | if err != nil { 318 | return fmt.Errorf("failed to start all the user programs and probes") 319 | } 320 | err = apis.StartConfigWatcher(ctx, machineHostname, daemonName, conf, ebpfConfigs) 321 | if err != nil { 322 | return fmt.Errorf("starting config Watcher failed") 323 | } 324 | err = handlers.InitConfigs(ebpfConfigs) 325 | if err != nil { 326 | return fmt.Errorf("l3afd failed to initialise configs") 327 | } 328 | if conf.EBPFChainDebugEnabled { 329 | bpfprogs.SetupBPFDebug(conf.EBPFChainDebugAddr, ebpfConfigs) 330 | } 331 | ebpfConfigs.ProcessMon.PCheckStart(ebpfConfigs.IngressXDPBpfs, ebpfConfigs.IngressTCBpfs, ebpfConfigs.EgressTCBpfs, &ebpfConfigs.ProbesBpfs) 332 | ebpfConfigs.BpfMetricsMon.BpfMetricsStart(ebpfConfigs.IngressXDPBpfs, ebpfConfigs.IngressTCBpfs, ebpfConfigs.EgressTCBpfs, &ebpfConfigs.ProbesBpfs) 333 | err = pidfile.CreatePID(conf.PIDFilename) 334 | if err != nil { 335 | return fmt.Errorf("the PID file: %s, could not be created", conf.PIDFilename) 336 | } 337 | return nil 338 | } 339 | 340 | func sendState(s string) { 341 | ln, err := net.Listen("unix", stateSockPath) 342 | if err != nil { 343 | log.Err(err) 344 | os.Exit(0) 345 | return 346 | } 347 | conn, err := ln.Accept() 348 | if err != nil { 349 | log.Err(err) 350 | ln.Close() 351 | os.Exit(0) 352 | return 353 | } 354 | encoder := gob.NewEncoder(conn) 355 | err = encoder.Encode(s) 356 | if err != nil { 357 | log.Err(err) 358 | conn.Close() 359 | ln.Close() 360 | os.Exit(0) 361 | return 362 | } 363 | conn.Close() 364 | ln.Close() 365 | } 366 | 367 | // populateVersions is to suppress codeql warning - Uncontrolled data used in network request 368 | func populateVersions(conf *config.Config) { 369 | models.AvailableVersions = make(map[string]string) 370 | for i := 0; i <= conf.VersionLimit; i++ { 371 | for j := 0; j <= conf.VersionLimit; j++ { 372 | for k := 0; k <= conf.VersionLimit; k++ { 373 | version := fmt.Sprintf("v%d.%d.%d", i, j, k) 374 | models.AvailableVersions[version] = version 375 | } 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/l3af-project/l3afd/v2/config" 11 | ) 12 | 13 | func TestTestConfigValid(t *testing.T) { 14 | // skip if file DNE 15 | f, err := os.Open("config/l3afd.cfg") 16 | if err != nil { 17 | t.Skip("l3afd.cfg not found") 18 | } 19 | f.Close() 20 | if _, err := config.ReadConfig("config/l3afd.cfg"); err != nil { 21 | t.Errorf("Unable to read l3afd config: %s", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mocks/mocked_interfaces.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: mock_interfaces.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "go.uber.org/mock/gomock" 11 | ) 12 | 13 | // MockplatformInterface is a mock of platformInterface interface. 14 | type MockplatformInterface struct { 15 | ctrl *gomock.Controller 16 | recorder *MockplatformInterfaceMockRecorder 17 | } 18 | 19 | // MockplatformInterfaceMockRecorder is the mock recorder for MockplatformInterface. 20 | type MockplatformInterfaceMockRecorder struct { 21 | mock *MockplatformInterface 22 | } 23 | 24 | // NewMockplatformInterface creates a new mock instance. 25 | func NewMockplatformInterface(ctrl *gomock.Controller) *MockplatformInterface { 26 | mock := &MockplatformInterface{ctrl: ctrl} 27 | mock.recorder = &MockplatformInterfaceMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockplatformInterface) EXPECT() *MockplatformInterfaceMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetPlatform mocks base method. 37 | func (m *MockplatformInterface) GetPlatform() (string, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "GetPlatform") 40 | ret0, _ := ret[0].(string) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // GetPlatform indicates an expected call of GetPlatform. 46 | func (mr *MockplatformInterfaceMockRecorder) GetPlatform() *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatform", reflect.TypeOf((*MockplatformInterface)(nil).GetPlatform)) 49 | } 50 | -------------------------------------------------------------------------------- /models/l3afd.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package models 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | // l3afd constants 11 | const ( 12 | Enabled = "enabled" 13 | Disabled = "disabled" 14 | 15 | StartType = "start" 16 | StopType = "stop" 17 | UpdateType = "update" 18 | 19 | XDPType = "xdp" 20 | TCType = "tc" 21 | 22 | IngressType = "ingress" 23 | EgressType = "egress" 24 | XDPIngressType = "xdpingress" 25 | TCMapPinPath = "tc/globals" 26 | 27 | KProbe = "kprobe" 28 | TracePoint = "tracepoint" 29 | KRetProbe = "kretprobe" 30 | UProbe = "uprobe" 31 | URetProbe = "uretprobe" 32 | ) 33 | 34 | type L3afDNFArgs map[string]interface{} 35 | 36 | // BPFProgram defines BPF Program for specific host 37 | type BPFProgram struct { 38 | ID int `json:"id"` // Program id 39 | Name string `json:"name"` // Name of the BPF program package 40 | SeqID int `json:"seq_id"` // Sequence position in the chain 41 | Artifact string `json:"artifact"` // Artifact file name 42 | MapName string `json:"map_name"` // BPF map to store next program fd 43 | CmdStart string `json:"cmd_start"` // Program start command 44 | CmdStop string `json:"cmd_stop"` // Program stop command 45 | CmdStatus string `json:"cmd_status"` // Program status command 46 | CmdConfig string `json:"cmd_config"` // Program config providing command 47 | CmdUpdate string `json:"cmd_update"` // Program update config command 48 | Version string `json:"version"` // Program version 49 | UserProgramDaemon bool `json:"user_program_daemon"` // User program daemon or not 50 | IsPlugin bool `json:"is_plugin"` // User program is plugin or not 51 | CPU int `json:"cpu"` // User program cpu limits 52 | Memory int `json:"memory"` // User program memory limits 53 | AdminStatus string `json:"admin_status"` // Program admin status enabled or disabled 54 | ProgType string `json:"prog_type"` // Program type XDP or TC 55 | RulesFile string `json:"rules_file"` // Config rules file name 56 | Rules string `json:"rules"` // Config rules 57 | ConfigFilePath string `json:"config_file_path"` // Config file location 58 | CfgVersion int `json:"cfg_version"` // Config version 59 | StartArgs L3afDNFArgs `json:"start_args"` // Map of arguments to start command 60 | StopArgs L3afDNFArgs `json:"stop_args"` // Map of arguments to stop command 61 | StatusArgs L3afDNFArgs `json:"status_args"` // Map of arguments to status command 62 | UpdateArgs L3afDNFArgs `json:"update_args"` // Map of arguments to update command 63 | MapArgs []L3afDMapArg `json:"map_args"` // Config BPF Map of arguments 64 | ConfigArgs L3afDNFArgs `json:"config_args"` // Map of arguments to config command 65 | MonitorMaps []L3afDNFMetricsMap `json:"monitor_maps"` // Metrics BPF maps 66 | EPRURL string `json:"ebpf_package_repo_url"` // Download url for Program 67 | ObjectFile string `json:"object_file"` // Object file contains BPF code 68 | EntryFunctionName string `json:"entry_function_name"` // BPF entry function name to load 69 | } 70 | 71 | // L3afDNFMetricsMap defines BPF map 72 | type L3afDNFMetricsMap struct { 73 | Name string `json:"name"` // BPF map name 74 | Key int `json:"key"` // Index of the bpf map 75 | Aggregator string `json:"aggregator"` // Aggregation function names 76 | } 77 | 78 | // KeyValue defines struct for key and value 79 | type KeyValue struct { 80 | Key int `json:"key"` // Key 81 | Value int `json:"value"` // Value 82 | } 83 | 84 | // L3afDMapArg defines map arg 85 | type L3afDMapArg struct { 86 | Name string `json:"name"` // BPF map name 87 | Args []KeyValue `json:"args"` // BPF map arguments 88 | } 89 | 90 | // L3afBPFPrograms defines configs for a node 91 | type L3afBPFPrograms struct { 92 | HostName string `json:"host_name"` // Host name or pod name 93 | Iface string `json:"iface"` // Interface name 94 | BpfPrograms *BPFPrograms `json:"bpf_programs"` // List of bpf programs 95 | } 96 | 97 | // BPFPrograms for a node 98 | type BPFPrograms struct { 99 | XDPIngress []*BPFProgram `json:"xdp_ingress"` // list of xdp ingress bpf programs 100 | TCIngress []*BPFProgram `json:"tc_ingress"` // list of tc ingress bpf programs 101 | TCEgress []*BPFProgram `json:"tc_egress"` // list of tc egress bpf programs 102 | Probes []*BPFProgram `json:"probes"` // list of probe bpf programs 103 | } 104 | 105 | // L3afBPFProgramNames defines names of Bpf programs on interface 106 | type L3afBPFProgramNames struct { 107 | HostName string `json:"host_name"` // Host name or pod name 108 | Iface string `json:"iface"` // Interface name 109 | BpfProgramNames *BPFProgramNames `json:"bpf_programs"` // List of eBPF program names to remove 110 | } 111 | 112 | // BPFProgramNames defines names of eBPF programs on node 113 | type BPFProgramNames struct { 114 | XDPIngress []string `json:"xdp_ingress"` // names of the XDP ingress eBPF programs 115 | TCIngress []string `json:"tc_ingress"` // names of the TC ingress eBPF programs 116 | TCEgress []string `json:"tc_egress"` // names of the TC egress eBPF programs 117 | Probes []string `json:"probes"` // names of the probe eBPF programs 118 | } 119 | 120 | type MetaColl struct { 121 | Programs []string 122 | Maps []string 123 | } 124 | 125 | type MetaMetricsBPFMap struct { 126 | MapName string 127 | Key int 128 | Values []float64 129 | Aggregator string 130 | LastValue float64 131 | } 132 | 133 | type Label struct { 134 | Name string 135 | Value string 136 | } 137 | 138 | type MetricVec struct { 139 | MetricName string 140 | Labels []Label 141 | Value float64 142 | Type int32 143 | } 144 | 145 | type L3AFMetaData struct { 146 | Program BPFProgram 147 | FilePath string 148 | RestartCount int 149 | PrevMapNamePath string 150 | MapNamePath string 151 | ProgID uint32 152 | BpfMaps []string 153 | MetricsBpfMaps map[string]MetaMetricsBPFMap 154 | ProgMapCollection MetaColl 155 | ProgMapID uint32 156 | PrevProgMapID uint32 157 | Link bool 158 | } 159 | 160 | type L3AFALLHOSTDATA struct { 161 | HostName string 162 | HostInterfaces map[string]bool 163 | IngressXDPBpfs map[string][]*L3AFMetaData 164 | IngressTCBpfs map[string][]*L3AFMetaData 165 | EgressTCBpfs map[string][]*L3AFMetaData 166 | ProbesBpfs []L3AFMetaData 167 | Ifaces map[string]string 168 | AllStats []MetricVec 169 | } 170 | 171 | type RestartConfig struct { 172 | HostName string `json:"hostname"` 173 | Version string `json:"version"` 174 | } 175 | 176 | var CloseForRestart chan struct{} 177 | var AllNetListeners sync.Map 178 | var CurrentWriteReq int 179 | var StateLock sync.Mutex 180 | var IsReadOnly bool 181 | var AvailableVersions map[string]string 182 | 183 | const HttpScheme string = "http" 184 | const HttpsScheme string = "https" 185 | const FileScheme string = "file" 186 | const StatusFailed string = "Failed" 187 | const StatusReady string = "Ready" 188 | 189 | // Please Do not make changes in socketpaths because they are means of communication between graceful restarts 190 | const HostSock string = "/tmp/l3afd.sock" 191 | const StateSock string = "/tmp/l3afstate.sock" 192 | const L3AFDRestartArtifactName string = "l3afd.tar.gz" 193 | -------------------------------------------------------------------------------- /pidfile/pidfile.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package pidfile 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | func CheckPIDConflict(pidFilename string) error { 18 | log.Info().Msgf("Checking for another already running instance (using PID file \"%s\")...", pidFilename) 19 | pidFileContent, err := os.ReadFile(pidFilename) 20 | if err != nil { 21 | if os.IsNotExist(err) { 22 | log.Info().Msgf("OK, no PID file already exists at %s.", pidFilename) 23 | return nil 24 | } 25 | return fmt.Errorf("could not open PID file: %s, please manually remove; error: %v", pidFilename, err) 26 | } 27 | if len(pidFileContent) < 1 { 28 | log.Warn().Msgf("PID file already exists at %s, but it is empty... ignoring.", pidFilename) 29 | return nil 30 | } 31 | oldPIDString := string(pidFileContent) 32 | oldPID, err := strconv.Atoi(oldPIDString) 33 | if err != nil { 34 | return fmt.Errorf("PID file: %s, contained value: %s, which could not be parsed; error: %v", pidFilename, oldPIDString, err) 35 | } 36 | 37 | log.Info().Msgf("Found PID file with PID: %d; checking if it is this process: PID: %d", oldPID, os.Getpid()) 38 | if oldPID == os.Getpid() { 39 | log.Warn().Msgf("PID file already exists at %s, but it contains the current PID(%d)... ignoring.", pidFilename, oldPID) 40 | return nil 41 | } 42 | 43 | log.Info().Msgf("Found PID file with PID: %s; checking if process is running...", oldPIDString) 44 | process, err := os.FindProcess(oldPID) 45 | if err == nil { 46 | //On Linux, if sig is 0, then no signal is sent, but error checking is still performed; 47 | //this can be used to check for the existence of a process ID or process group ID. 48 | //See: man 2 kill 49 | err = process.Signal(syscall.Signal(0)) 50 | } 51 | if err != nil { 52 | log.Info().Msgf("Process was not running, removing PID file.") 53 | if err = RemovePID(pidFilename); err != nil { 54 | return fmt.Errorf("removal failed, please manually remove; err: %v", err) 55 | } 56 | return nil 57 | } 58 | 59 | log.Info().Msgf("Process with PID: %s; is running. Comparing process names to ensure it is a true conflict.", oldPIDString) 60 | selfProcName, err := os.ReadFile("/proc/self/comm") 61 | if err != nil { 62 | return fmt.Errorf("could not read this processes command name from the proc filesystem; err: %v", err) 63 | } 64 | conflictProcName, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", oldPID)) 65 | if err != nil { 66 | return fmt.Errorf("could not read old processes (PID: %s) command name from the proc filesystem; error: %v", oldPIDString, err) 67 | } 68 | if string(selfProcName) != string(conflictProcName) { 69 | log.Info().Msgf("Old process had command name: %q, not %q, removing PID file.", conflictProcName, selfProcName) 70 | if err = RemovePID(pidFilename); err != nil { 71 | return fmt.Errorf("removal failed, please manually remove; error: %v", err) 72 | } 73 | return nil 74 | } 75 | 76 | return fmt.Errorf("a previous instance of this process (%s) is running with ID %s; please shutdown this process before running", selfProcName, oldPIDString) 77 | } 78 | 79 | func CreatePID(pidFilename string) error { 80 | PID := os.Getpid() 81 | log.Info().Msgf("Writing process ID %d to %s...", PID, pidFilename) 82 | if err := os.WriteFile(pidFilename, []byte(strconv.Itoa(PID)), 0640); err != nil { 83 | return fmt.Errorf("could not write process ID to file: \"%s\"; error: %v", pidFilename, err) 84 | } 85 | return nil 86 | } 87 | 88 | func RemovePID(pidFilename string) error { 89 | err := os.RemoveAll(pidFilename) 90 | if err != nil { 91 | err = fmt.Errorf("could not remove PID file: %s; error: %v", pidFilename, err) 92 | } 93 | return err 94 | } 95 | 96 | func SetupGracefulShutdown(shutdownHandler func() error, shutdownHandlerTimeout time.Duration, pidFilename string) { 97 | const defaultShutdownTO = time.Second * 10 98 | if shutdownHandlerTimeout < 1 { 99 | log.Warn().Msgf("GracefulShutdown: No shutdown timeout was provided! Using %s.", defaultShutdownTO) 100 | shutdownHandlerTimeout = defaultShutdownTO 101 | } 102 | 103 | //We must use a buffered channel or risk missing the signal if we're not 104 | //ready to receive when the signal is sent. 105 | interruptCh := make(chan os.Signal, 1) 106 | signal.Notify(interruptCh, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT) 107 | 108 | //Start worker that listens for shutdown signal and handles it 109 | go func() { 110 | interrupt := <-interruptCh 111 | exitCode := 0 112 | 113 | if shutdownHandler != nil { //Run shutdown handler 114 | log.Info().Msgf("GracefulShutdown: Received shutdown signal: %s, waiting for shutdown handler to execute (will timeout after %s)...", interrupt, shutdownHandlerTimeout) 115 | handlerDoneCh := make(chan struct{}) 116 | go func() { 117 | if err := shutdownHandler(); err != nil { 118 | log.Error().Err(err).Msgf("GracefulShutdown: Shutdown handler returned error") 119 | exitCode = 1 120 | } 121 | handlerDoneCh <- struct{}{} 122 | }() 123 | select { 124 | case <-handlerDoneCh: 125 | log.Info().Msgf("GracefulShutdown: Shutdown handler execution complete. Shutting down...") 126 | case <-time.After(shutdownHandlerTimeout): 127 | log.Error().Msgf("GracefulShutdown: Shutdown handler execution timed-out after %s! Shutting down...", shutdownHandlerTimeout) 128 | exitCode = 1 129 | } 130 | } else { 131 | log.Info().Msgf("GracefulShutdown: Received shutdown signal: %s, shutting down...", interrupt) 132 | } 133 | 134 | if pidFilename != "" { 135 | if err := RemovePID(pidFilename); err != nil { 136 | log.Warn().Err(err).Msgf("Could not cleanup PID file") 137 | } 138 | } 139 | log.Info().Msgf("Shutdown now.") 140 | os.Exit(exitCode) 141 | }() 142 | } 143 | -------------------------------------------------------------------------------- /register_internal.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !admind 5 | // +build !admind 6 | 7 | // This file is used for walmart internal to register with management server. 8 | // We will be removing this file in future. 9 | 10 | package main 11 | 12 | import ( 13 | "github.com/l3af-project/l3afd/v2/config" 14 | 15 | "github.com/rs/zerolog/log" 16 | ) 17 | 18 | func registerL3afD(conf *config.Config) error { 19 | log.Warn().Msg("Implement custom registration with management server") 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /restart/restart_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package restart 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/l3af-project/l3afd/v2/models" 10 | ) 11 | 12 | func TestGetValueofLabel(t *testing.T) { 13 | b := []models.Label{ 14 | { 15 | Name: "iface", 16 | Value: "fakeif0", 17 | }, 18 | } 19 | q := "iface" 20 | ans := "fakeif0" 21 | t.Run("goodtest", func(t *testing.T) { 22 | if ans != getValueofLabel(q, b) { 23 | t.Errorf("GetValueofLabel failed") 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /routes/route.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package routes 5 | 6 | import "net/http" 7 | 8 | // Route defines a valid endpoint with the type of action supported on it 9 | type Route struct { 10 | Method string 11 | Path string 12 | HandlerFunc http.HandlerFunc 13 | } 14 | -------------------------------------------------------------------------------- /routes/router.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package routes 5 | 6 | import ( 7 | chi "github.com/go-chi/chi/v5" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | // NewRouter returns a router handle loaded with all the supported routes 12 | func NewRouter(routes []Route) *chi.Mux { 13 | r := chi.NewRouter() 14 | 15 | for _, route := range routes { 16 | r.Method(route.Method, route.Path, route.HandlerFunc) 17 | log.Info().Msgf("Route added:%+v\n", route) 18 | } 19 | 20 | return r 21 | } 22 | -------------------------------------------------------------------------------- /signals/signal_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build !WINDOWS 5 | // +build !WINDOWS 6 | 7 | package signals 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | var ShutdownSignals = []os.Signal{os.Interrupt, 15 | syscall.SIGHUP, 16 | syscall.SIGINT, 17 | syscall.SIGTERM, 18 | syscall.SIGQUIT} 19 | -------------------------------------------------------------------------------- /signals/signal_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | // 4 | //go:build WINDOWS 5 | // +build WINDOWS 6 | 7 | package signals 8 | 9 | import ( 10 | "os" 11 | ) 12 | 13 | var ShutdownSignals = []os.Signal{os.Interrupt} 14 | -------------------------------------------------------------------------------- /stats/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package stats 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "net/http" 10 | 11 | "github.com/l3af-project/l3afd/v2/models" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promauto" 14 | "github.com/prometheus/client_golang/prometheus/promhttp" 15 | "github.com/rs/zerolog/log" 16 | ) 17 | 18 | var ( 19 | BPFStartCount *prometheus.CounterVec 20 | BPFStopCount *prometheus.CounterVec 21 | BPFUpdateCount *prometheus.CounterVec 22 | BPFUpdateFailedCount *prometheus.CounterVec 23 | BPFRunning *prometheus.GaugeVec 24 | BPFStartTime *prometheus.GaugeVec 25 | BPFMonitorMap *prometheus.GaugeVec 26 | BPFDeployFailedCount *prometheus.CounterVec 27 | ) 28 | 29 | func SetupMetrics(hostname, daemonName, metricsAddr string) { 30 | 31 | bpfStartCountVec := promauto.NewCounterVec( 32 | prometheus.CounterOpts{ 33 | Namespace: daemonName, 34 | Name: "BPFStartCount", 35 | Help: "The count of BPF program started", 36 | }, 37 | []string{"host", "ebpf_program", "direction", "interface_name"}, 38 | ) 39 | 40 | BPFStartCount = bpfStartCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 41 | 42 | bpfStopCountVec := promauto.NewCounterVec( 43 | prometheus.CounterOpts{ 44 | Namespace: daemonName, 45 | Name: "BPFStopCount", 46 | Help: "The count of BPF program stopped", 47 | }, 48 | []string{"host", "ebpf_program", "direction", "interface_name"}, 49 | ) 50 | 51 | BPFStopCount = bpfStopCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 52 | 53 | bpfUpdateCountVec := promauto.NewCounterVec( 54 | prometheus.CounterOpts{ 55 | Namespace: daemonName, 56 | Name: "BPFUpdateCount", 57 | Help: "The count of BPF programs updated", 58 | }, 59 | []string{"host", "ebpf_program", "direction", "interface_name"}, 60 | ) 61 | 62 | BPFUpdateCount = bpfUpdateCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 63 | 64 | bpfUpdateFailedCountVec := promauto.NewCounterVec( 65 | prometheus.CounterOpts{ 66 | Namespace: daemonName, 67 | Name: "BPFUpdateFailedCount", 68 | Help: "The count of Failed BPF program update args", 69 | }, 70 | []string{"host", "bpf_program", "direction", "interface_name"}, 71 | ) 72 | 73 | BPFUpdateFailedCount = bpfUpdateFailedCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 74 | 75 | bpfRunningVec := prometheus.NewGaugeVec( 76 | prometheus.GaugeOpts{ 77 | Namespace: daemonName, 78 | Name: "BPFRunning", 79 | Help: "This value indicates BPF program is running or not", 80 | }, 81 | []string{"host", "ebpf_program", "version", "direction", "interface_name"}, 82 | ) 83 | 84 | if err := prometheus.Register(bpfRunningVec); err != nil { 85 | log.Warn().Err(err).Msg("Failed to register BPFRunning metrics") 86 | } 87 | 88 | BPFRunning = bpfRunningVec.MustCurryWith(prometheus.Labels{"host": hostname}) 89 | 90 | bpfStartTimeVec := prometheus.NewGaugeVec( 91 | prometheus.GaugeOpts{ 92 | Namespace: daemonName, 93 | Name: "BPFStartTime", 94 | Help: "This value indicates start time of the BPF program since unix epoch in seconds", 95 | }, 96 | []string{"host", "ebpf_program", "direction", "interface_name"}, 97 | ) 98 | 99 | if err := prometheus.Register(bpfStartTimeVec); err != nil { 100 | log.Warn().Err(err).Msg("Failed to register BPFStartTime metrics") 101 | } 102 | 103 | BPFStartTime = bpfStartTimeVec.MustCurryWith(prometheus.Labels{"host": hostname}) 104 | 105 | bpfMonitorMapVec := prometheus.NewGaugeVec( 106 | prometheus.GaugeOpts{ 107 | Namespace: daemonName, 108 | Name: "BPFMonitorMap", 109 | Help: "This value indicates BPF program monitor counters", 110 | }, 111 | []string{"host", "ebpf_program", "map_name", "interface_name"}, 112 | ) 113 | 114 | if err := prometheus.Register(bpfMonitorMapVec); err != nil { 115 | log.Warn().Err(err).Msg("Failed to register BPFMonitorMap metrics") 116 | } 117 | 118 | BPFMonitorMap = bpfMonitorMapVec.MustCurryWith(prometheus.Labels{"host": hostname}) 119 | 120 | BPFDeployFailedCountVec := promauto.NewCounterVec( 121 | prometheus.CounterOpts{ 122 | Namespace: daemonName, 123 | Name: "BPFDeployFailedCount", 124 | Help: "The count of BPF program failed to start or update", 125 | }, 126 | []string{"host", "ebpf_program", "direction", "interface_name"}, 127 | ) 128 | BPFDeployFailedCount = BPFDeployFailedCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 129 | 130 | BPFStartCount = bpfStartCountVec.MustCurryWith(prometheus.Labels{"host": hostname}) 131 | // Prometheus handler 132 | metricsHandler := promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}) 133 | // Adding web endpoint 134 | go func() { 135 | // Expose the registered metrics via HTTP. 136 | if _, ok := models.AllNetListeners.Load("stat_http"); !ok { 137 | tcpAddr, err := net.ResolveTCPAddr("tcp", metricsAddr) 138 | if err != nil { 139 | log.Fatal().Err(err).Msg("Error resolving TCP address") 140 | return 141 | } 142 | listener, err := net.ListenTCP("tcp", tcpAddr) 143 | if err != nil { 144 | log.Fatal().Err(err).Msgf("unable to create net Listen") 145 | } 146 | models.AllNetListeners.Store("stat_http", listener) 147 | } 148 | http.Handle("/metrics", metricsHandler) 149 | val, _ := models.AllNetListeners.Load("stat_http") 150 | l, _ := val.(*net.TCPListener) 151 | if err := http.Serve(l, nil); !errors.Is(err, http.ErrServerClosed) { 152 | log.Fatal().Err(err).Msgf("Failed to launch prometheus metrics endpoint") 153 | } 154 | }() 155 | } 156 | 157 | func Add(value float64, counterVec *prometheus.CounterVec, ebpfProgram, direction, ifaceName string) { 158 | 159 | if counterVec == nil { 160 | log.Warn().Msg("Metrics: counter vector is nil and needs to be initialized before Incr") 161 | return 162 | } 163 | bpfCounter, err := counterVec.GetMetricWith( 164 | prometheus.Labels(map[string]string{ 165 | "ebpf_program": ebpfProgram, 166 | "direction": direction, 167 | "interface_name": ifaceName, 168 | }), 169 | ) 170 | if err != nil { 171 | log.Warn().Msgf("Metrics: unable to fetch counter with fields: ebpf_program: %s, direction: %s, interface_name: %s", 172 | ebpfProgram, direction, ifaceName) 173 | return 174 | } 175 | bpfCounter.Add(value) 176 | } 177 | 178 | func Set(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, direction, ifaceName string) { 179 | 180 | if gaugeVec == nil { 181 | log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before Set") 182 | return 183 | } 184 | bpfGauge, err := gaugeVec.GetMetricWith( 185 | prometheus.Labels(map[string]string{ 186 | "ebpf_program": ebpfProgram, 187 | "direction": direction, 188 | "interface_name": ifaceName, 189 | }), 190 | ) 191 | if err != nil { 192 | log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, direction: %s, interface_name: %s", 193 | ebpfProgram, direction, ifaceName) 194 | return 195 | } 196 | bpfGauge.Set(value) 197 | } 198 | 199 | // Set gaugevec metrics value with given mapName and other fields 200 | func SetValue(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, mapName, ifaceName string) { 201 | 202 | if gaugeVec == nil { 203 | log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before SetValue") 204 | return 205 | } 206 | bpfGauge, err := gaugeVec.GetMetricWith( 207 | prometheus.Labels(map[string]string{ 208 | "ebpf_program": ebpfProgram, 209 | "map_name": mapName, 210 | "interface_name": ifaceName, 211 | }), 212 | ) 213 | if err != nil { 214 | log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, map_name: %s, interface_name: %s", 215 | ebpfProgram, mapName, ifaceName) 216 | return 217 | } 218 | bpfGauge.Set(value) 219 | } 220 | 221 | func SetWithVersion(value float64, gaugeVec *prometheus.GaugeVec, ebpfProgram, version, direction, ifaceName string) { 222 | 223 | if gaugeVec == nil { 224 | log.Warn().Msg("Metrics: gauge vector is nil and needs to be initialized before Set") 225 | return 226 | } 227 | bpfGauge, err := gaugeVec.GetMetricWith( 228 | prometheus.Labels(map[string]string{ 229 | "ebpf_program": ebpfProgram, 230 | "version": version, 231 | "direction": direction, 232 | "interface_name": ifaceName, 233 | }), 234 | ) 235 | if err != nil { 236 | log.Warn().Msgf("Metrics: unable to fetch gauge with fields: ebpf_program: %s, version: %s, direction: %s, interface_name: %s", 237 | ebpfProgram, version, direction, ifaceName) 238 | return 239 | } 240 | bpfGauge.Set(value) 241 | } 242 | -------------------------------------------------------------------------------- /testdata/Test_l3af-config.json: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /testdata/l3afd.cdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l3af-project/l3afd/be00758bc8eaa56e1102b9ff3d32a284322c04c0/testdata/l3afd.cdb -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright Contributors to the L3AF Project. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package utils provides helper functions for l3afd 5 | package utils 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // GetKernelVersion - reads the kernel version from /proc/version 16 | func GetKernelVersion() (string, error) { 17 | osVersion, err := os.ReadFile("/proc/version") 18 | if err != nil { 19 | return "", fmt.Errorf("failed to read procfs: %v", err) 20 | } 21 | var u1, u2, kernelVersion string 22 | _, err = fmt.Sscanf(string(osVersion), "%s %s %s", &u1, &u2, &kernelVersion) 23 | if err != nil { 24 | return "", fmt.Errorf("failed to scan procfs version: %v", err) 25 | } 26 | 27 | return kernelVersion, nil 28 | } 29 | 30 | // CheckTCXSupport - verifies kernel version is 6.8 or above 31 | func CheckTCXSupport() bool { 32 | const minVerLen = 2 33 | const minMajorKernelVer = 6 34 | const minMinorKernelVer = 8 35 | 36 | kernelVersion, err := GetKernelVersion() 37 | if err != nil { 38 | return false 39 | } 40 | 41 | // validate version 42 | ver := strings.Split(kernelVersion, ".") 43 | if len(ver) < minVerLen { 44 | return false 45 | } 46 | majorVer, err := strconv.Atoi(ver[0]) 47 | if err != nil { 48 | return false 49 | } 50 | minorVer, err := strconv.Atoi(ver[1]) 51 | if err != nil { 52 | return false 53 | } 54 | 55 | if majorVer > minMajorKernelVer { 56 | return true 57 | } 58 | if majorVer == minMajorKernelVer && minorVer >= minMinorKernelVer { 59 | return true 60 | } 61 | 62 | return false 63 | } 64 | 65 | // ReplaceDotsWithUnderscores replaces all dots in the given string with underscores. 66 | func ReplaceDotsWithUnderscores(version string) string { 67 | return strings.ReplaceAll(version, ".", "_") 68 | } 69 | 70 | // LinkPinPath builds the formatted link pin path string. 71 | func LinkPinPath(bpfMapDefaultPath, ifaceName, programName, version, progType string) string { 72 | return filepath.Join(bpfMapDefaultPath, "links", ifaceName, programName, version, fmt.Sprintf("%s_%s", programName, progType)) 73 | } 74 | 75 | // TCLinkPinPath builds the formatted link pin path string. 76 | func TCLinkPinPath(bpfMapDefaultPath, ifaceName, programName, version, progType, direction string) string { 77 | return filepath.Join(bpfMapDefaultPath, "links", ifaceName, programName, version, fmt.Sprintf("%s_%s_%s", programName, progType, direction)) 78 | } 79 | 80 | // ProgPinPath builds the formatted program pin path string. 81 | func ProgPinPath(bpfMapDefaultPath, ifaceName, programName, version, entryFunctionName, progType string) string { 82 | return filepath.Join(bpfMapDefaultPath, "progs", ifaceName, programName, version, fmt.Sprintf("%s_%s", entryFunctionName, progType)) 83 | } 84 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "time" 9 | ) 10 | 11 | var ( 12 | // Version binary version number 13 | Version string = "2.1.0" 14 | // SuffixTag binary tag (e.g. beta, dev, devel, production). 15 | SuffixTag string 16 | // VersionDate build timestamp (e.g. ). 17 | VersionDate string 18 | // VersionSHA git commit SHA. 19 | VersionSHA string 20 | 21 | versionFlag = flag.Bool("version", false, "display version information") 22 | ) 23 | 24 | // Init prints version information and exits, if --version option passed. 25 | func initVersion() { 26 | if *versionFlag { 27 | fmt.Println(VersionInfo()) 28 | os.Exit(0) 29 | } 30 | } 31 | 32 | // VersionInfo returns a formatted string of version data used in the version flag. 33 | func VersionInfo() string { 34 | var sha, buildDate string 35 | if btime, err := time.Parse("20060102150405", VersionDate); err == nil { 36 | buildDate = "built " + btime.Format(time.RFC3339) 37 | } else { 38 | currentFilePath, err := os.Executable() 39 | if err == nil { 40 | info, err := os.Stat(currentFilePath) 41 | if err == nil { 42 | buildDate = info.ModTime().Format(time.RFC3339) 43 | } 44 | } 45 | } 46 | 47 | if VersionSHA != "" { 48 | sha = "\nBuild SHA: " + VersionSHA 49 | } 50 | 51 | return fmt.Sprintf("Version: %s\nGo Version: %s\nBuild Date: %s%s", ShortVersion(), runtime.Version(), buildDate, sha) 52 | } 53 | 54 | // ShortVersion returns a formatted string containing only the Version and VersionTag 55 | func ShortVersion() string { 56 | if Version == "0.0.0" { 57 | SuffixTag = "dev" 58 | } 59 | if SuffixTag != "" { 60 | return fmt.Sprintf("%s-%s", Version, SuffixTag) 61 | } 62 | return Version 63 | } 64 | --------------------------------------------------------------------------------