├── .github ├── ISSUE_TEMPLATE │ ├── BUG-ISSUE.yml │ ├── FEATURE-ISSUE.yml │ ├── config.yml │ └── question.md └── workflows │ ├── issue_board.yaml │ ├── pull_request.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── Vagrantfile ├── bee ├── Dockerfile-bee └── main.go ├── builder ├── Dockerfile ├── build.sh ├── builder.go ├── solo_types.h └── vmlinux.h ├── ci └── release_assets.go ├── docs ├── bee_running.png ├── concepts.md ├── contributing.md └── getting_started.md ├── examples ├── activeconn │ └── activeconn.c ├── capable │ ├── README.md │ └── capable.c ├── exitsnoop │ ├── README.md │ ├── exitsnoop.c │ └── exitsnoop.h ├── kprobetcp │ └── handler.c ├── nfsstats │ └── nfsstats.c ├── oomkill │ ├── README.md │ └── oomkill.c ├── opensnoop │ ├── README.md │ ├── opensnoop.c │ └── opensnoop.h ├── tcpconnect │ └── tcpconnect.c └── tcpconnlat │ ├── README.md │ └── tcpconnlat.c ├── go.mod ├── go.sum ├── install_cli.sh ├── logo.svg ├── pkg ├── cli │ ├── app.go │ └── internal │ │ ├── commands │ │ ├── build │ │ │ └── build.go │ │ ├── describe │ │ │ └── describe.go │ │ ├── initialize │ │ │ ├── init.go │ │ │ └── templates.go │ │ ├── list │ │ │ └── list.go │ │ ├── login │ │ │ └── login.go │ │ ├── package │ │ │ ├── Dockerfile │ │ │ └── package.go │ │ ├── pull │ │ │ └── pull.go │ │ ├── push │ │ │ └── push.go │ │ ├── run │ │ │ └── run.go │ │ ├── tag │ │ │ └── tag.go │ │ └── version │ │ │ └── version.go │ │ └── options │ │ └── opts.go ├── decoder │ ├── decoder.go │ ├── loader_be.go │ └── loader_le.go ├── internal │ └── version │ │ └── version.go ├── loader │ ├── loader.go │ └── watcher.go ├── spec │ ├── array.o │ ├── consts.go │ ├── packaging_suite_test.go │ ├── spec.go │ ├── spec_test.go │ └── utils.go ├── stats │ └── stats.go └── tui │ ├── filter.go │ └── tui.go └── spec └── README.md /.github/ISSUE_TEMPLATE/BUG-ISSUE.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | labels: ["Type: Bug"] 4 | body: 5 | 6 | - type: input 7 | id: version 8 | attributes: 9 | label: Version 10 | description: What version of Bumblebee are you using? (output of `bee version`) 11 | validations: 12 | required: true 13 | - type: input 14 | id: linux-version 15 | attributes: 16 | label: Linux Version 17 | description: If eBPF related, what version of the Linux kernel are you using? (output of `uname -r`) 18 | - type: textarea 19 | id: describe-bug 20 | attributes: 21 | label: Describe the bug 22 | placeholder: Add a clear and concise description of what the bug is 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: reproduce-steps 27 | attributes: 28 | label: Steps to reproduce the bug 29 | placeholder: | 30 | 1. bee build '...' 31 | 1. bee run '....' 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: expected-behavior 36 | attributes: 37 | label: Expected Behavior 38 | placeholder: When I performed x, I expected y to happen 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: additional-context 43 | attributes: 44 | label: Additional Context 45 | placeholder: | 46 | Additional information specific to this issue 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-ISSUE.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: ["Type: Enhancement"] 4 | body: 5 | - type: input 6 | id: version 7 | attributes: 8 | label: Version 9 | description: What version of Bumblebee are you using? (output of `bee version`) 10 | - type: input 11 | id: linux-version 12 | attributes: 13 | label: Linux Version 14 | description: What version of the Linux kernel are you using? (output of `uname -r`) 15 | - type: textarea 16 | id: problem-details 17 | attributes: 18 | label: Is your feature request related to a problem? Please describe. 19 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 20 | - type: textarea 21 | id: solution-details 22 | attributes: 23 | label: Describe the solution you'd like 24 | placeholder: A clear and concise description of what you want to happen. 25 | - type: textarea 26 | id: alternatives-details 27 | attributes: 28 | label: Describe alternatives you've considered 29 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 30 | - type: textarea 31 | id: additional-context 32 | attributes: 33 | label: Additional Context 34 | placeholder: Add any other context or screenshots about the feature request here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Talk with us on Slack 4 | url: https://solo-io.slack.com/app_redirect?channel=C02TEN3MV3J 5 | about: For a fast response, consider asking your question on the Solo slack. 6 | - name: Bumblebee Documentation 7 | url: https://github.com/solo-io/bumblebee/tree/main/docs 8 | about: Check out the Bumblebee docs for helpful information. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Asking a question \ seeking guidance 4 | title: '' 5 | labels: 'Type: Question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | For a fast response, consider asking your question on the [bumblebee slack channel](https://solo-io.slack.com/app_redirect?channel=C02TEN3MV3J) channel! Get an invite at https://slack.solo.io. 11 | -------------------------------------------------------------------------------- /.github/workflows/issue_board.yaml: -------------------------------------------------------------------------------- 1 | name: issue_board 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | jobs: 8 | add-to-project: 9 | name: Add Bumblebee to Gloo Mesh. 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/add-to-project@main 13 | with: 14 | project-url: https://github.com/orgs/solo-io/projects/23 15 | github-token: ${{ secrets.ORG_CROSS_REPO }} 16 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - 'main' 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: lint 10 | runs-on: ubuntu-24.04 11 | steps: 12 | - name: Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.4.0 14 | with: 15 | access_token: ${{ github.token }} 16 | - uses: actions/checkout@v2 17 | - run: | 18 | git fetch --prune --unshallow 19 | - name: Set up Go 1.18 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: 1.18.10 23 | - uses: actions/cache@v4 24 | with: 25 | path: ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | - name: lint 30 | run: | 31 | go mod tidy 32 | if [[ $(git status --porcelain --untracked-files=no | wc -l) -ne 0 ]]; then 33 | echo "Need to run go mod tidy before committing" 34 | git diff 35 | exit 1; 36 | fi 37 | test: 38 | name: test 39 | runs-on: ubuntu-24.04 40 | steps: 41 | - name: Cancel Previous Runs 42 | uses: styfle/cancel-workflow-action@0.4.0 43 | with: 44 | access_token: ${{ github.token }} 45 | - uses: actions/checkout@v2 46 | - run: | 47 | git fetch --prune --unshallow 48 | - name: Set up Go 1.18 49 | uses: actions/setup-go@v4 50 | with: 51 | go-version: 1.18.10 52 | - uses: actions/cache@v4 53 | with: 54 | path: ~/go/pkg/mod 55 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 56 | restore-keys: | 57 | ${{ runner.os }}-go- 58 | - name: build 59 | run: | 60 | go build ./bee/main.go 61 | - name: test 62 | run: | 63 | go test ./... -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | release-assets: 13 | name: Github Release Assets 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - name: Cancel Previous Runs 17 | uses: styfle/cancel-workflow-action@0.4.0 18 | with: 19 | access_token: ${{ github.token }} 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | - run: git fetch --prune --unshallow 23 | - name: Set up Go 1.18 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: 1.18.10 27 | - name: Setup Cache 28 | uses: actions/cache@v4 29 | with: 30 | path: ~/go/pkg/mod 31 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 32 | restore-keys: ${{ runner.os }}-go- 33 | - name: Upload Assets 34 | run: make upload-github-release-assets 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 37 | TAGGED_VERSION: ${{ github.event.release.tag_name }} 38 | 39 | push-builder-images: 40 | runs-on: ubuntu-latest 41 | permissions: 42 | contents: read 43 | packages: write 44 | 45 | steps: 46 | - name: Checkout repository 47 | uses: actions/checkout@v4 48 | 49 | - name: Set up QEMU 50 | uses: docker/setup-qemu-action@master 51 | with: 52 | platforms: all 53 | - name: Set up Docker Buildx 54 | id: buildx 55 | uses: docker/setup-buildx-action@master 56 | 57 | - name: Log in to the Container registry 58 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.actor }} 62 | password: ${{ secrets.PUSH_TOKEN_NEW }} 63 | - name: Push Docker Container 64 | run: make docker-push 65 | env: 66 | TAGGED_VERSION: ${{ github.event.release.tag_name }} 67 | 68 | push-bee-image: 69 | runs-on: ubuntu-latest 70 | permissions: 71 | contents: read 72 | packages: write 73 | 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v4 77 | - name: Set up Go 1.18 78 | uses: actions/setup-go@v4 79 | with: 80 | go-version: 1.18.10 81 | - name: Setup Cache 82 | uses: actions/cache@v4 83 | with: 84 | path: ~/go/pkg/mod 85 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 86 | restore-keys: ${{ runner.os }}-go- 87 | - name: Log in to the Container registry 88 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 89 | with: 90 | registry: ghcr.io 91 | username: ${{ github.actor }} 92 | password: ${{ secrets.PUSH_TOKEN_NEW }} 93 | - name: Push bee container 94 | run: make docker-push-bee 95 | env: 96 | TAGGED_VERSION: ${{ github.event.release.tag_name }} 97 | 98 | push-example-programs: 99 | runs-on: ubuntu-latest 100 | permissions: 101 | contents: read 102 | packages: write 103 | 104 | steps: 105 | - name: Checkout repository 106 | uses: actions/checkout@v4 107 | 108 | - name: Set up Go 1.18 109 | uses: actions/setup-go@v4 110 | with: 111 | go-version: 1.18.10 112 | - name: Setup Cache 113 | uses: actions/cache@v4 114 | with: 115 | path: ~/go/pkg/mod 116 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 117 | restore-keys: ${{ runner.os }}-go- 118 | 119 | - name: Log in to the Container registry 120 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 121 | with: 122 | registry: ghcr.io 123 | username: ${{ github.actor }} 124 | password: ${{ secrets.PUSH_TOKEN_NEW }} 125 | 126 | - name: Build builder container 127 | run: make docker-build 128 | env: 129 | TAGGED_VERSION: ${{ github.event.release.tag_name }} 130 | 131 | - name: Build CLI 132 | run: make bee-linux-amd64 133 | env: 134 | TAGGED_VERSION: ${{ github.event.release.tag_name }} 135 | 136 | - name: Push example images 137 | run: make release-examples -B 138 | env: 139 | TAGGED_VERSION: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Example outputs 15 | examples/**/*.o 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | vendor/ 19 | _output 20 | 21 | # Staging ground for bpf programs 22 | bpf/ 23 | 24 | # Ignore IDE folders 25 | .vscode/ 26 | .vagrant/ 27 | .idea -------------------------------------------------------------------------------- /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 2017 Solo.io, Inc. 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 | #---------------------------------------------------------------------------------- 2 | # Versioning 3 | #---------------------------------------------------------------------------------- 4 | OUTDIR?=_output 5 | HUB?=ghcr.io/solo-io 6 | REPO_NAME?=bumblebee 7 | EXAMPLES_DIR?=examples 8 | DOCKER := docker 9 | 10 | RELEASE := "true" 11 | ifeq ($(TAGGED_VERSION),) 12 | TAGGED_VERSION := $(shell git describe --tags --dirty --always) 13 | RELEASE := "false" 14 | endif 15 | # In iron mountain escrow action we pass in the tag as TAGGED_VERSION: ${{ github.ref }}. 16 | # This tag has the refs/tags prefix, which we need to remove here. 17 | export VERSION ?= $(shell echo $(TAGGED_VERSION) | sed -e "s/^refs\/tags\///" | cut -c 2-) 18 | 19 | LDFLAGS := "-X github.com/solo-io/bumblebee/pkg/internal/version.Version=$(VERSION)" 20 | GCFLAGS := all="-N -l" 21 | 22 | SOURCES := $(shell find . -name "*.go" | grep -v test.go) 23 | 24 | .PHONY: clean 25 | clean: 26 | rm -f $(EXAMPLES_DIR)/**/*.o 27 | rm -rf $(OUTDIR) 28 | 29 | #---------------------------------------------------------------------------------- 30 | # Build Container 31 | #---------------------------------------------------------------------------------- 32 | PUSH_CMD:= 33 | PLATFORMS?=linux/amd64 34 | docker-build: 35 | # may run into issues with apt-get and the apt.llvm.org repo, in which case use --no-cache to build 36 | # e.g. `docker build --no-cache ./builder -f builder/Dockerfile -t $(HUB)/bumblebee/builder:$(VERSION) 37 | $(DOCKER) build --platform $(PLATFORMS) $(PUSH_CMD) ./builder -f builder/Dockerfile -t $(HUB)/bumblebee/builder:$(VERSION) 38 | 39 | docker-push: PUSH_CMD=--push 40 | docker-push: DOCKER=docker buildx 41 | docker-push: PLATFORMS=linux/amd64,linux/arm64/v8 42 | docker-push: docker-build 43 | 44 | #---------------------------------------------------------------------------------- 45 | # Examples 46 | #---------------------------------------------------------------------------------- 47 | 48 | .PHONY: activeconn 49 | activeconn: $(EXAMPLES_DIR)/activeconn 50 | .PHONY: tcpconnect 51 | tcpconnect: $(EXAMPLES_DIR)/tcpconnect 52 | .PHONY: exitsnoop 53 | exitsnoop: $(EXAMPLES_DIR)/exitsnoop 54 | .PHONY: oomkill 55 | oomkill: $(EXAMPLES_DIR)/oomkill 56 | .PHONY: capable 57 | capable: $(EXAMPLES_DIR)/capable 58 | .PHONY: tcpconnlat 59 | tcpconnlat: $(EXAMPLES_DIR)/tcpconnlat 60 | 61 | 62 | $(EXAMPLES_DIR)/%: 63 | $(OUTDIR)/bee-linux-amd64 build $@/$*.c $(HUB)/$(REPO_NAME)/$*:$(VERSION) 64 | $(OUTDIR)/bee-linux-amd64 push $(HUB)/$(REPO_NAME)/$*:$(VERSION) 65 | 66 | .PHONY: release-examples 67 | release-examples: activeconn tcpconnect exitsnoop oomkill capable tcpconnlat 68 | 69 | #---------------------------------------------------------------------------------- 70 | # CLI 71 | #---------------------------------------------------------------------------------- 72 | 73 | 74 | $(OUTDIR)/bee-linux-amd64: $(SOURCES) 75 | CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) -o $@ bee/main.go 76 | 77 | .PHONY: bee-linux-amd64 78 | bee-linux-amd64: $(OUTDIR)/bee-linux-amd64.sha256 79 | $(OUTDIR)/bee-linux-amd64.sha256: $(OUTDIR)/bee-linux-amd64 80 | sha256sum $(OUTDIR)/bee-linux-amd64 > $@ 81 | 82 | $(OUTDIR)/bee-linux-arm64: $(SOURCES) 83 | CGO_ENABLED=0 GOARCH=arm64 GOOS=linux go build -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) -o $@ bee/main.go 84 | 85 | .PHONY: bee-linux-arm64 86 | bee-linux-arm64: $(OUTDIR)/bee-linux-arm64.sha256 87 | $(OUTDIR)/bee-linux-arm64.sha256: $(OUTDIR)/bee-linux-arm64 88 | sha256sum $(OUTDIR)/bee-linux-arm64 > $@ 89 | 90 | .PHONY: build-cli 91 | build-cli: bee-linux-amd64 bee-linux-arm64 92 | 93 | .PHONY: install-cli 94 | install-cli: 95 | CGO_ENABLED=0 go install -ldflags=$(LDFLAGS) -gcflags=$(GCFLAGS) ./bee 96 | 97 | BEE_DIR := bee 98 | $(OUTDIR)/Dockerfile-bee: $(BEE_DIR)/Dockerfile-bee 99 | cp $< $@ 100 | 101 | .PHONY: docker-build-bee 102 | docker-build-bee: build-cli $(OUTDIR)/Dockerfile-bee 103 | $(DOCKER) build $(OUTDIR) -f $(OUTDIR)/Dockerfile-bee -t $(HUB)/bumblebee/bee:$(VERSION) 104 | 105 | .PHONY: docker-push-bee 106 | docker-push-bee: docker-build-bee 107 | $(DOCKER) push $(HUB)/bumblebee/bee:$(VERSION) 108 | 109 | ##---------------------------------------------------------------------------------- 110 | ## Release 111 | ##---------------------------------------------------------------------------------- 112 | 113 | .PHONY: upload-github-release-assets 114 | upload-github-release-assets: build-cli 115 | ifeq ($(RELEASE),"true") 116 | go run ci/release_assets.go 117 | endif 118 | 119 | .PHONY: regen-vmlinux 120 | regen-vmlinux: 121 | bpftool btf dump file /sys/kernel/btf/vmlinux format c > builder/vmlinux.h 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Bumblebee 3 |

4 | 5 | BumbleBee helps to build, run and distribute eBPF programs using OCI images. It allows you to focus on writing eBPF code, while taking care of the user space components - automatically exposing your data as metrics or logs. 6 | 7 | Join the conversation [on our Slack](https://solo-io.slack.com/) in the [#bumblebee](https://solo-io.slack.com/app_redirect?channel=C02TEN3MV3J) channel! 8 | If you need an invite to join, please [try here](https://slack.solo.io/). 9 | 10 | ### Documentation 11 | - **Installation** 12 | - [Install Bee](#Installation) 13 | - **Getting Started** 14 | - [What is eBPF?](https://ebpf.io/what-is-ebpf) 15 | - [Ramp up on BumbleBee concepts](docs/concepts.md) 16 | - [Write your first BumbleBee program](docs/getting_started.md) 17 | - **Developer Documentation** 18 | - [Contributing](docs/contributing.md) 19 | 20 | --- 21 | ## Getting Started 22 | 23 | The first step to get started is to install `bee` using one of the [installation](#Installation) techniques listed below. 24 | 25 | Once `bee` has been installed we can go ahead and initialize our first `eBPF` probe! To do this you just need to run one command! (no seriously that's it.) 26 | *Note*: This will only work on linux. If you don't have access to a linux machine, [Vagrant](https://learn.hashicorp.com/tutorials/vagrant/getting-started-install) will work as well! 27 | 28 | ```bash 29 | sudo bee run ghcr.io/solo-io/bumblebee/tcpconnect:$(bee version) 30 | ``` 31 | To see data populated simply run `curl httpbin.org` in another window. 32 | 33 | *Note*: If you installed `bee` via the install script you may see an issue with the `bee` command not being available when run with `sudo`. 34 | See the [note on permissions](#a-note-on-permissions) for more info, but to quickly get started you can use the following command as an alternative: 35 | 36 | ```bash 37 | sudo env "PATH=$PATH" bee run ghcr.io/solo-io/bumblebee/tcpconnect:$(bee version) 38 | ``` 39 | 40 | Now that you have run your first example program, you can go ahead and write your own with our [tutorial](docs/getting_started.md). 41 | 42 | ## Installation 43 | 44 | ### Using our install script 45 | 46 | Download the latest version: 47 | 48 | ```bash 49 | curl -sL https://run.solo.io/bee/install | sh 50 | ``` 51 | 52 | Or download a given version (e.g. v0.0.8): 53 | 54 | ```bash 55 | curl -sL https://run.solo.io/bee/install | BUMBLEBEE_VERSION=v0.0.8 sh 56 | ``` 57 | 58 | ### Using go 59 | ```bash 60 | git clone git@github.com:solo-io/bumblebee.git 61 | cd bumblebee 62 | # install to GOBIN (defaults to ~/go/bin/), can be used as `bee` if GOBIN is on your PATH 63 | go install ./bee 64 | ``` 65 | 66 | #### Other options 67 | 68 | You can also navigate to the releases page [here](https://github.com/solo-io/bumblebee/releases/) for more versions/information. 69 | 70 | ### A note on permissions 71 | 72 | Loading eBPF programs to the kernel (`bee run` command) requires elevated privileges. 73 | You can either run `bee` as root (with sudo), or add capabilities to the binary. 74 | Adding capabilities is the preferred method, as if you run `bee run` with `sudo`, it will not find local images when you run `bee build` without sudo. 75 | 76 | To add capabilities, run the following command: 77 | 78 | ```bash 79 | sudo setcap cap_sys_resource,cap_sys_admin+eip $(which bee) 80 | ``` 81 | 82 | On newer kernels you may be able to pare down the capabilities added, for example a recent addition is the `CAP_BPF` capability. 83 | 84 | ### Uninstall 85 | 86 | If you installed `bee` via the install script, you can uninstall `bee` by removing the directory: 87 | 88 | ```bash 89 | rm -rf $HOME/.bumblebee/ 90 | ``` 91 | 92 | If you used `bee` as root or via sudo, remove the following directory: 93 | 94 | ```bash 95 | rm -rf /root/.bumblebee 96 | ``` 97 | 98 | ### License 99 | 100 | Apache 2 101 | 102 | ### Thanks 103 | 104 | This project would not be possible without the valuable open-source work of projects in the eBPF community. Specifically, we would like to thank the [eBPF go](https://github.com/cilium/ebpf/) library and [libbpf-tools](https://github.com/iovisor/bcc/tree/master/libbpf-tools/). 105 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 |

2 | Bumblebee 3 |

4 | 5 | The following items are on our roadmap: 6 | 7 | - Language support: 8 | - Add Rust 9 | - Add golang 10 | - Kubernetes integration: 11 | - Enable easy interaction with Kubernetes objects such as pods 12 | - Add support for [user space control](https://github.com/solo-io/bumblebee/issues/68) 13 | - Support for multi-arch builds 14 | - Exposing filters in the eBPF program itself to filter output through the `bee run` command 15 | - Support additional BPF program and map types 16 | - Support histogram Prometheus metric type 17 | - Add support for integration with logging pipelines (e.g. in structured format to stdout) 18 | 19 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | 2 | Vagrant.configure("2") do |config| 3 | # amd64 4 | config.vm.box = "generic/ubuntu2110" 5 | # arm64 6 | # config.vm.box = "nickschuetz/ubuntu-21.10-arm64" 7 | config.vm.define :impish 8 | config.vm.hostname = "impish" 9 | config.vm.synced_folder ".", "/source" 10 | config.vm.provision "shell", inline: <<-SHELL 11 | # install llvm: 12 | export DEBIAN_FRONTEND=noninteractive 13 | export LLVM_VERSION=13 14 | curl -sL https://apt.llvm.org/llvm.sh "$LLVM_VERSION" | bash 15 | apt-get -qq update 16 | # bpf related deps: 17 | apt-get -qq install linux-headers-$(uname -r) linux-tools-$(uname -r) libbpf-dev 18 | # dev tools: 19 | apt-get -qq install -y golang docker.io make 20 | usermod -aG docker vagrant 21 | # add headers: 22 | bpftool btf dump file /sys/kernel/btf/vmlinux format c > /usr/local/include/vmlinux.h 23 | cp /source/builder/solo_types.h /usr/local/include/ 24 | SHELL 25 | end 26 | -------------------------------------------------------------------------------- /bee/Dockerfile-bee: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14 2 | 3 | # installs public root certs 4 | RUN apk upgrade --update-cache \ 5 | && apk add ca-certificates \ 6 | && rm -rf /var/cache/apk/* 7 | 8 | ADD bee-linux-amd64 bee 9 | -------------------------------------------------------------------------------- /bee/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/solo-io/bumblebee/pkg/cli" 7 | "oras.land/oras-go/pkg/context" 8 | ) 9 | 10 | func main() { 11 | // Use context with discarded logrus logger so we don't fill our logs unecessarily 12 | ctx := context.Background() 13 | if err := cli.Bee().ExecuteContext(ctx); err != nil { 14 | log.Fatalf("exiting: %s", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | LABEL org.opencontainers.image.source https://github.com/solo-io/bumblebee 4 | 5 | RUN apt-get update &&\ 6 | apt-get -y install lsb-release wget software-properties-common gnupg file git make 7 | 8 | RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - 9 | RUN add-apt-repository "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-13 main" 10 | RUN apt-get update 11 | 12 | RUN apt-get install -y clang-13 lldb-13 lld-13 clangd-13 man-db 13 | RUN apt-get install -y bpftool libbpf-dev 14 | 15 | # non package installed default include directory 16 | # Note, you can run "make regen-vmlinux" to re-generate this file 17 | ADD vmlinux.h /usr/local/include/ 18 | 19 | # Ensure that solo helper types are available from workdir 20 | ADD solo_types.h /usr/local/include/ 21 | 22 | ADD build.sh /usr/local/bin 23 | 24 | RUN mkdir /usr/src/bpf/ 25 | WORKDIR /usr/src/bpf 26 | 27 | ENTRYPOINT ["build.sh"] 28 | -------------------------------------------------------------------------------- /builder/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -eux 3 | 4 | CFLAGS=${CFLAGS:-} 5 | 6 | clang-13 -g -O2 -target bpf -D__TARGET_ARCH_x86 ${CFLAGS} -Wall -c $1 -o $2 7 | 8 | # strip debug sections (see: https://github.com/libbpf/libbpf-bootstrap/blob/94000ca67c5e7be4741c09c435c9ae1777822378/examples/c/Makefile#L65) 9 | llvm-strip-13 -g $2 10 | -------------------------------------------------------------------------------- /builder/builder.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import _ "embed" 4 | 5 | //go:embed build.sh 6 | var buildScript []byte 7 | 8 | func GetBuildScript() []byte { 9 | return buildScript 10 | } 11 | -------------------------------------------------------------------------------- /builder/solo_types.h: -------------------------------------------------------------------------------- 1 | // Basic type definitions for use with solo eBPF programs 2 | 3 | 4 | // A basic ipv4 address represented as a u32 5 | typedef u32 ipv4_addr; 6 | // A basic ipv6 address represented as a u32 7 | typedef u32 ipv6_addr; 8 | // A duration in NS stored as a u64 9 | typedef u64 duration; -------------------------------------------------------------------------------- /ci/release_assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/solo-io/go-utils/githubutils" 5 | ) 6 | 7 | func main() { 8 | const buildDir = "_output" 9 | const repoOwner = "solo-io" 10 | const repoName = "bumblebee" 11 | 12 | assets := []githubutils.ReleaseAssetSpec{ 13 | { 14 | Name: "bee-linux-amd64", 15 | ParentPath: buildDir, 16 | UploadSHA: true, 17 | }, 18 | { 19 | Name: "bee-linux-arm64", 20 | ParentPath: buildDir, 21 | UploadSHA: true, 22 | }, 23 | } 24 | spec := githubutils.UploadReleaseAssetSpec{ 25 | Owner: repoOwner, 26 | Repo: repoName, 27 | Assets: assets, 28 | SkipAlreadyExists: true, 29 | } 30 | githubutils.UploadReleaseAssetCli(&spec) 31 | } 32 | -------------------------------------------------------------------------------- /docs/bee_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solo-io/bumblebee/82747bf94621117ff338d75d745c6834062f5581/docs/bee_running.png -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | ## Building 4 | 5 | BumbleBee by default uses a containerized build environment to build your BPF programs to an ELF file, then packages that in an OCI image according to our image spec. 6 | 7 | Additionally, if desired, you can then package your BPF program as a standard Docker image that contains the `bee` CLI/runner in addition to your BPF programs. 8 | The end result is a standard docker image that can be distributed via standard docker-like workflows to run your BPF program anywhere you run containerized workloads, such as a K8s cluster. 9 | Note that you will need sufficient capabilities to run the image, as loading and running the BPF program is a privileged operation for most intents and purposes. 10 | 11 | An example workflow is as follows: 12 | ```bash 13 | $ bee build examples/tcpconnect/tcpconnect.c tcpconnect 14 | SUCCESS Successfully compiled "examples/tcpconnect/tcpconnect.c" and wrote it to "examples/tcpconnect/tcpconnect.o" 15 | SUCCESS Saved BPF OCI image to tcpconnect 16 | 17 | $ bee package tcpconnect bee-tcpconnect:latest 18 | SUCCESS Packaged image built and tagged at bee-tcpconnect:latest 19 | 20 | # run the bee-tcpconnect:latest image, deploy to K8s, etc. 21 | $ docker run --privileged bee-tcpconnect:latest 22 | ``` 23 | 24 | Note that the `--privileged` flag is used to provide the necessary permissions (alternatively this can be scoped down via capabilities through your system/orchestrator). 25 | Since this will typically not be used interactively, by default the `CMD` for the container is `bee run --no-tty` which will not render the TUI. 26 | Metrics can be scraped from this container to provide insight to your maps. 27 | 28 | ## BPF conventions 29 | 30 | `BPF` programs are typically made up of 2 main parts: 31 | 1. The maps which allow the user space and kernel space programs to share data. 32 | 2. The functions which can be attached to kernel probes and tracepoints. 33 | 34 | For more detailed examples of these, please see our [tutorial](getting_started.md). This section will discuss the additional features and conventions we have added on top of this workflow. 35 | 36 | ### Maps 37 | 38 | As the `bee` runner is primarily targeted at observability, much of the user space functionality of the tool is centered around the maps. The extension of the maps allows our user space runner to interpret and process the data from these maps in a generic way. The two main types of maps which are supported at this time are `RingBuffer` and `HashMap`. There is some overlap in the functionality of the two within our runner, but also some important differences. 39 | 40 | **Important Note:** Currently all structs used in maps which are meant to be processed by our user space runner cannot be nested. This may be added in the future for the logging/eventing, but not for metrics. 41 | 42 | #### RingBuffer 43 | 44 | `RingBuffer` is a generic map type which traditionally allows for temporary storage of many arbitrary data types. This allows the kernel or user space program to feed data into them, which can be read out in order from the other. In the case of `bee` the direction will be `kernel -> user`. In order to be able to generically handle this data however, we have imposed a restriction that only one type of data may be stored in the RingBuffer. This may change in the future. 45 | 46 | In order to specify the type of data to be stored in the RingBuffer, it can be added to the `BPF` map definition. Typically it is not valid to store the type in a `RingBuffer` map definition, as there can be multiple types, but in this case it allows us to properly parse the data, and that type never makes it into the kernel map definition. 47 | ```C 48 | struct { 49 | __uint(type, BPF_MAP_TYPE_RINGBUF); 50 | __uint(max_entries, 1 << 24); 51 | __type(value, struct event_t); 52 | } print_events SEC(".maps"); 53 | ``` 54 | 55 | The other aspect of the above program worth noting is its name: `print_events SEC(".maps")`. Specifically the `print_` prefix. Please see the [output formats](#Output-Formats) section below for more info. The `RingBuffer` map type supports the `print_` and `counter_` prefix. 56 | 57 | The final thing worth noting about the `RingBuffer` is it's event based nature. Each object is handled only once, and then never read from again. This differs from the `HashMap`, which will be discussed in greater detail below. 58 | 59 | #### HashMap 60 | 61 | Like `RingBuffer` above, `HashMap` is a generic map type to store data, with some key differences. The `HashMap` does not function as a queue, but rather as a traditional map, with both keys and values, which retains it's data until manually removed. 62 | 63 | In addition, `HashMap` supports section keywords to enable special [output formats](#Output-Formats). The valid prefixes for this type of map are: `print_`, `counter_`, and `gauge_`. 64 | 65 | 66 | ### Programs 67 | 68 | Nothing specific has been added on top of the BPF programs/functions themselves at this time. 69 | 70 | 71 | ## Output Formats 72 | 73 | Part of what makes `bee` so special, as mentioned above, is that it allows us to write `eBPF` probes with 0 user space code. In fact it allows for easy translation of kernel data and events into metrics and logging. As mentioned above this is accomplished through the use of special conventions and keywords. Before reading this section, we recommend reading the [conventions](#BPF-conventions) above for a brief overview. 74 | 75 | These special conventions and keywords come in the form of additional kernel code additions, some in section names, and some to the code itself. Let's begin with the section names. 76 | 77 | Maps in `BPF` programs are defined using the `SEC(".maps")` keyword. When running using the `bee` runner, extra prefixes to its name can be added to describe how this data should be handled. These can be roughly broken down into 2 behaviors, metrics and logging. Metrics turns the data into prometheus metrics which can be consumed by any standard prometheus deployments. And logging which emits structured json logs with the provided data, and can be consumed by any structured logging applications. 78 | 79 | The second convention we have added is a set of `typedef`s which describe to our runner how the underlying type is meant to be processed after it leaves the kernel. These are stored in a file called `solo_types.h` and are made available automatically when building with `bee`. Some examples include: 80 | ```C 81 | // A basic ipv4 address represented as a u32 82 | typedef u32 ipv4_addr; 83 | // A basic ipv6 address represented as a u32 84 | typedef u32 ipv6_addr; 85 | // A duration in NS stored as a u64 86 | typedef u64 duration; 87 | ``` 88 | 89 | These types can be used in the structs which populate our maps to instruct the runner to treat the values in a special way. For instance, any `duration` value will be processed in the user space program as a golang `time.Duration` and then can be printed, and tracked as such. 90 | 91 | 92 | ### Logging 93 | 94 | Logging may be the simplest output format of our `eBPF` probes, but it is also incredibly powerful for both observability and debugging. 95 | Logging in our system comes in two main forms: event based and timer based. 96 | The type of logging used is based on the underlying map type. 97 | When logging a `RingBuffer` each event is handled/logged individually as it is received and therefore it will only be printed once. 98 | However, when using a `HashMap`, the data is polled on an interval. Therefore, the logging will happen on each interval and only when there is a change in the values of the map from the previous interval. 99 | 100 | When using `bee` to run your BPF programs, the TUI that is rendered by default will automatically handle the printing of the data in maps that follow the naming conventions. 101 | In other words, printing/logging of data is handled for metric output types automatically. 102 | We have a TCP-based example that demonstrates both map types which will explore more in depth. 103 | You can find the source in our examples folder here: [`/examples/tcpconnect/tcpconnect.c`](/examples/tcpconnect/tcpconnect.c). 104 | Both map types below use the following `struct` to define the shape of data being processed by our maps: 105 | ```C 106 | struct dimensions_t { 107 | ipv4_addr saddr; 108 | ipv4_addr daddr; 109 | } __attribute__((packed)); 110 | ``` 111 | This struct contains two IPv4 addresses, a source and destination IP. 112 | 113 | #### RingBuffer 114 | 115 | Looking at the `RingBuffer` map in our `tcpconnect` program: 116 | ```C 117 | struct { 118 | __uint(type, BPF_MAP_TYPE_RINGBUF); 119 | __uint(max_entries, 1 << 24); 120 | __type(value, struct dimensions_t); 121 | } counter_events_ring SEC(".maps"); 122 | ``` 123 | We can see that the `struct dimensions_t` type is being passed into the `RingBuffer`. 124 | So as TCP connections are established, the source and destination IP of these connections will be sent as events into this `RingBuffer`. Also note the `counter_` prefix to its name. 125 | This tells `bee` to "watch" this map and log the data (in addition to emitting counter metrics, which will explore more in a later section). 126 | 127 | When running the program, as new TCP connections happen (by e.g. making a `curl 1.1.1.1` request in a separate terminal) we can see the TUI log the data: 128 | ``` 129 | daddr saddr 130 | 1.1.1.1 10.128.0.119 131 | ``` 132 | The data in contained is not particularly interesting, but rather the formatting and structure. 133 | As the connection was created and data sent to our map, we will dynamically get the data printed to our screen in the correct format! 134 | 135 | #### HashMap 136 | 137 | Looking at the `RingBuffer` map in our `tcpconnect` program: 138 | ```C 139 | struct { 140 | __uint(type, BPF_MAP_TYPE_HASH); 141 | __uint(max_entries, 8192); 142 | __type(key, struct dimensions_t); 143 | __type(value, u64); 144 | } counter_events_hash SEC(".maps"); 145 | ``` 146 | 147 | As above, we can see that the `struct dimensions_t` type is being used, this time as the `key` type for our `HashMap`. 148 | When running the program, we can see the entries get updated as connections are created: 149 | ``` 150 | daddr saddr value 151 | 1.1.1.1 10.128.0.119 1 152 | ``` 153 | This one differs slightly from the `RingBuffer` example above in a couple important ways. 154 | First of all the log lines do not happen at the same frequency as the events themselves, but rather on a timer. 155 | Secondly, the values in the key (`daddr` and `saddr`) are printed in addition to the `value`, which represents the total count of connections for this given source/destination pair. 156 | As these values change, or new source/destination pairs are introduced, the value will update and new rows will be printed accordingly. 157 | 158 | ### Metrics 159 | 160 | Potentially even more powerful than the logging features of the `bee` runner are it's metrics capabilities. As opposed to the logging feature, the metrics feature allows for creation and export of generic metrics + labels from `eBPF` probes. A couple simple, yet powerful, examples of this functionality are in the `examples` folder. `activeconn` keeps track of all active tcpv4 connections in a gauge with source/dest IP as the metric labels. The `tcpconnect` example does something similar, but it increments a counter for each new connection, rather than maintaining all active. 161 | 162 | #### Counter 163 | 164 | Currently there are 2 ways to use a counter with `bee`. One with a `HashMap` and one with a `RingBuffer`. 165 | 166 | An example of the both the `RingBuffer` counter and `HashMap` counter exist in the `examples/tcpconnect` folder. The program tracks the number of TCP connections using both map types to illustrate their use. We do not recommend saving the same value two separate ways. 167 | 168 | After starting the program, and curling httpbin a few times we can, we can get the metrics from `curl localhost:9091/metrics | grep events` 169 | ``` 170 | # HELP ebpf_solo_io_counter_events_hash 171 | # TYPE ebpf_solo_io_counter_events_hash counter 172 | ebpf_solo_io_counter_events_hash{daddr="18.232.227.86",saddr="10.128.0.79"} 9 173 | ebpf_solo_io_counter_events_hash{daddr="3.216.167.140",saddr="10.128.0.79"} 5 174 | # HELP ebpf_solo_io_counter_events_ring 175 | # TYPE ebpf_solo_io_counter_events_ring counter 176 | ebpf_solo_io_counter_events_ring{daddr="18.232.227.86",saddr="10.128.0.79"} 9 177 | ebpf_solo_io_counter_events_ring{daddr="3.216.167.140",saddr="10.128.0.79"} 5 178 | ``` 179 | 180 | As we can see the number of connections are being tracked both from our `HashMap` and `RingBuffer` implementation. 181 | 182 | #### Gauge 183 | 184 | Gauges are used to track numeric values that can change over time. 185 | BumbleBee supports automatically exporting gauge style metrics for both `RingBuffer` and `HashMap` type maps as long as your map is correctly defined with a name with a `gauge_` prefix. 186 | 187 | An example of a gauge is the number of active connections to a given host. 188 | The [/examples/activeconn/activeconn.c](/examples/activeconn/activeconn.c) file contains an implementation of active connection tracking by using a `HashMap` type map with a `gauge_` output type. 189 | 190 | Let's take a closer look at the `struct` which defines the map that will contain the connection counts. 191 | ```c 192 | struct { 193 | __uint(type, BPF_MAP_TYPE_HASH); 194 | __uint(max_entries, 8192); 195 | __type(key, struct dimensions_t); 196 | __type(value, u64); 197 | } gauge_sockets_ext SEC(".maps"); 198 | ``` 199 | 200 | This defines a `HashMap` containing integer values for connection counts which are keyed by `struct dimensions_t` (which we explored in the [HashMap](#hashmap-1) section). 201 | In other words, this means that each source and destination address pair will point to an integer representing the current number of active connections. 202 | 203 | The exporting of metrics is automatically handled thanks to the name prefix of `gauge_`. 204 | This tells the `bee` runner to export gauge metrics of the current value for each entry in the `HashMap` map each time the value of the map is polled. 205 | Alternatively, if we were using a `RingBuffer` with gauge output, when each entry is processed by the `bee` runner, the gauge value will be updated accordingly. 206 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Developing `eBPF` does not require a Linux machine, however running the probes does. `eBPF` itself is a Linux kernel technology, therefore any actual `BPF` programs must run in the Linux kernel. We are working on an OSX development path, but it has not been completed as of yet. 4 | 5 | We recommend doing `eBPF` development on a Linux machine. Do not fret however if you don't have a native Linux desktop, neither do we. Using `vscode` and GCP allows for a seamless near native development experience. See the following [article](https://safwene-benaich.medium.com/developing-on-remote-vm-via-vscode-using-google-clouds-iap-6b6549f9270c) for more detail. The article details the steps on a Windows machine, but they should be nearly identical on a macOS. 6 | 7 | Also worth noting , `bee` does not currently support ARM architectures (coming soon). However, the above development trick should alleviate that issue in the short-term. 8 | 9 | ## Repo Structure 10 | 11 | The following is a brief overview of the internal code structure 12 | 13 | ```. 14 | ├── builder ## Dockerfile and scripts related to our eBPF build container 15 | ├── ci ## Scripts and helpers for CI 16 | ├── docs ## Docs and other useful information for interacting with bumblebee 17 | ├── bee ## main.go file for bee, majority of code is in pkg 18 | ├── examples ## Variety of example eBPF programs to be run with bee 19 | ├── pkg ## Primary code directory 20 | └── spec ## Contains information related to eBPF OCI Spec 21 | ``` 22 | 23 | ## Development 24 | 25 | For non-Linux users, we have a [Vagrant](https://learn.hashicorp.com/tutorials/vagrant/getting-started-install) box available for you to easily get started with a Linux environment. 26 | 27 | After checking out the source code of the bumblebee repo, you can simply start the Vagrant VM and SSH into the VM: 28 | 29 | ```bash 30 | cd bumblebee 31 | vagrant up 32 | vagrant ssh 33 | ``` 34 | 35 | This bumblebee folder will be mounted under "/source" in the vagrant VM. This VM has [BTF](https://www.kernel.org/doc/html/latest/bpf/btf.html) already enabled, along with all other required tool chains such as Docker, Go, LLVM, etc. Refer to its [Vagrantfile](/Vagrantfile) for more details. 36 | 37 | For fast iterations of go code / bpf programs, you can build with our build script, and run with go run as follows: 38 | 39 | ```bash 40 | cd /source 41 | ./builder/build.sh ./examples/tcpconnect/tcpconnect.c tcpconnect.o 42 | go run -exec sudo ./bee/main.go run tcpconnect.o 43 | ``` 44 | 45 | To make a local docker image for the `bee` to use, you can run: 46 | 47 | ```bash 48 | make docker-build 49 | ``` 50 | 51 | Or, if for podman: 52 | 53 | ```bash 54 | make docker-build DOCKER=podman 55 | ``` 56 | 57 | You can then use the docker image in the `bee build` command, for example: 58 | 59 | ```bash 60 | bee build examples/tcpconnect/tcpconnect.c tcpconnect.o -i $DOCKER_BUILT_IMAGE 61 | ``` 62 | 63 | ### Workflow for `bee package` dev 64 | 65 | ``` 66 | TAGGED_VERSION=vdev make docker-build-bee 67 | 68 | go run ./bee/main.go build -i ghcr.io/solo-io/bumblebee/builder:0.0.9 examples/tcpconnect/tcpconnect.c tcpconnect 69 | 70 | go run ./bee/main.go package tcpconnect bee-tcpconnect:v1 71 | 72 | docker run --privileged bee-tcpconnect:v1 73 | ``` 74 | -------------------------------------------------------------------------------- /examples/activeconn/activeconn.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "solo_types.h" 3 | #include "bpf/bpf_helpers.h" 4 | #include "bpf/bpf_core_read.h" 5 | #include "bpf/bpf_tracing.h" 6 | 7 | char __license[] SEC("license") = "Dual MIT/GPL"; 8 | 9 | struct dimensions_t { 10 | ipv4_addr saddr; 11 | ipv4_addr daddr; 12 | } __attribute__((packed)); 13 | 14 | struct { 15 | __uint(type, BPF_MAP_TYPE_HASH); 16 | __uint(max_entries, 8192); 17 | __type(key, u32); 18 | __type(value, struct sock *); 19 | } sockets SEC(".maps"); 20 | 21 | struct { 22 | __uint(type, BPF_MAP_TYPE_HASH); 23 | __uint(max_entries, 8192); 24 | __type(key, struct dimensions_t); 25 | __type(value, u64); 26 | } gauge_sockets_ext SEC(".maps"); 27 | 28 | static __always_inline int 29 | enter_tcp_connect(struct pt_regs *ctx, struct sock *sk) 30 | { 31 | __u64 pid_tgid = bpf_get_current_pid_tgid(); 32 | __u32 tid = pid_tgid; 33 | bpf_printk("enter called"); 34 | 35 | bpf_printk("enter: setting sk for tid: %u", tid); 36 | bpf_map_update_elem(&sockets, &tid, &sk, 0); 37 | return 0; 38 | } 39 | 40 | static __always_inline int 41 | record(struct pt_regs *ctx, int ret, int op) 42 | { 43 | __u64 pid_tgid = bpf_get_current_pid_tgid(); 44 | __u32 tid = pid_tgid; 45 | struct sock **skpp; 46 | struct sock *sk; 47 | 48 | __u32 saddr; 49 | __u32 daddr; 50 | u64 val; 51 | u64 *valp; 52 | struct dimensions_t key = {}; 53 | 54 | bpf_printk("exit: getting sk for tid: '%u', ret is: '%d'", tid, ret); 55 | skpp = bpf_map_lookup_elem(&sockets, &tid); 56 | if (!skpp) { 57 | bpf_printk("exit: no pointer for tid, returning: %u", tid); 58 | return 0; 59 | } 60 | sk = *skpp; 61 | 62 | bpf_printk("exit: found sk for tid: %u", tid); 63 | BPF_CORE_READ_INTO(&saddr, sk, __sk_common.skc_rcv_saddr); 64 | BPF_CORE_READ_INTO(&daddr, sk, __sk_common.skc_daddr); 65 | key.saddr = saddr; 66 | key.daddr = daddr; 67 | 68 | valp = bpf_map_lookup_elem(&gauge_sockets_ext, &key); 69 | if (!valp) { 70 | bpf_printk("no entry for {saddr: %u, daddr: %u}", key.saddr, key.daddr); 71 | val = 1; 72 | } 73 | else { 74 | bpf_printk("found existing value '%llu' for {saddr: %u, daddr: %u}", *valp, key.saddr, key.daddr); 75 | val = *valp + op; 76 | } 77 | bpf_map_update_elem(&gauge_sockets_ext, &key, &val, 0); 78 | bpf_map_delete_elem(&sockets, &tid); 79 | return 0; 80 | } 81 | 82 | SEC("kprobe/tcp_v4_connect") 83 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 84 | { 85 | return enter_tcp_connect(ctx, sk); 86 | } 87 | 88 | SEC("kretprobe/tcp_v4_connect") 89 | int BPF_KRETPROBE(tcp_v4_connect_ret, int ret) 90 | { 91 | return record(ctx, ret, 1); 92 | } 93 | 94 | SEC("kprobe/tcp_close") 95 | int BPF_KPROBE(tcp_close, struct sock *sk) 96 | { 97 | return enter_tcp_connect(ctx, sk); 98 | } 99 | 100 | SEC("kretprobe/tcp_close") 101 | int BPF_KRETPROBE(tcp_close_ret, int ret) 102 | { 103 | return record(ctx, ret, -1); 104 | } 105 | 106 | -------------------------------------------------------------------------------- /examples/capable/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The capable example is heavily based on the [capable program in BCC repository](https://github.com/iovisor/bcc/blob/master/tools/capable.py), which is created by Brendan Gregg. 4 | This eBPF program will trace all security capability checks (cap_capable() calls) in your system. 5 | 6 | # Usage 7 | 8 | To see all the syscalls in your environment, you can run your image without filters: 9 | 10 | ```console 11 | bee run ghcr.io/solo-io/bumblebee/capable:$(bee version) 12 | ``` 13 | 14 | You can also try out the filtering capability by referencing fields in your BPF map: 15 | 16 | ```c 17 | struct cap_event { 18 | __u64 mntnsid; 19 | __u32 pid; 20 | int cap; 21 | __u32 tgid; 22 | __u32 uid; 23 | int cap_opt; 24 | char task[TASK_COMM_LEN]; 25 | }; 26 | ``` 27 | 28 | For example, to filter for all the capability checks where the `task` is `ping`, you can use: 29 | 30 | ```console 31 | bee run -f="print_events,task,ping" ghcr.io/solo-io/bumblebee/capable:$(bee version) 32 | ``` 33 | 34 | # Prometheus integration 35 | 36 | Let's say, you want to visualize the rate of such syscalls in your Prometheus stack, or want to alert on certain syscalls. 37 | 38 | You can modify your `print_events` map to generate a `counter` from your cap_capable() calls: 39 | 40 | > Note: you can rename `events` to `cap_events` to illustrate the goal of the exposed events better. 41 | 42 | ```c 43 | struct { 44 | __uint(type, BPF_MAP_TYPE_RINGBUF); 45 | __uint(max_entries, 1 << 24); 46 | __type(value, struct cap_event); 47 | } counter_events SEC(".maps"); 48 | ``` 49 | 50 | You should consider removing high cardinality fields from your map to avoid overloading your Prometheus instance, e.g. `mntnsid`. 51 | -------------------------------------------------------------------------------- /examples/capable/capable.c: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/kinvolk/inspektor-gadget/blob/main/pkg/gadgets/capabilities/tracer/core/bpf/capable.bpf.c 2 | // Copyright 2022 Sony Group Corporation 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define TASK_COMM_LEN 16 10 | #define MAX_ENTRIES 10240 11 | 12 | struct cap_event { 13 | __u64 mntnsid; 14 | __u32 pid; 15 | int cap; 16 | __u32 tgid; 17 | __u32 uid; 18 | int cap_opt; 19 | char task[TASK_COMM_LEN]; 20 | }; 21 | 22 | struct key_t { 23 | __u32 pid; 24 | __u32 tgid; 25 | int user_stack_id; 26 | int kern_stack_id; 27 | }; 28 | 29 | struct { 30 | __uint(type, BPF_MAP_TYPE_RINGBUF); 31 | __uint(max_entries, 1 << 24); 32 | __type(value, struct cap_event); 33 | } print_events SEC(".maps"); 34 | 35 | struct { 36 | __uint(type, BPF_MAP_TYPE_HASH); 37 | __type(key, struct key_t); 38 | __type(value, struct cap_event); 39 | __uint(max_entries, MAX_ENTRIES); 40 | } info SEC(".maps"); 41 | 42 | struct { 43 | __uint(type, BPF_MAP_TYPE_HASH); 44 | __uint(max_entries, 1024); 45 | __uint(key_size, sizeof(u64)); 46 | __uint(value_size, sizeof(u32)); 47 | } mount_ns_set SEC(".maps"); 48 | 49 | SEC("kprobe/cap_capable") 50 | int BPF_KPROBE(kprobe__cap_capable, const struct cred *cred, struct user_namespace *targ_ns, int cap, int cap_opt) 51 | { 52 | __u32 pid; 53 | u64 mntns_id; 54 | __u64 pid_tgid; 55 | struct key_t i_key; 56 | struct task_struct *task; 57 | 58 | task = (struct task_struct*) bpf_get_current_task(); 59 | mntns_id = (u64) BPF_CORE_READ(task, nsproxy, mnt_ns, ns.inum); 60 | 61 | pid_tgid = bpf_get_current_pid_tgid(); 62 | pid = pid_tgid >> 32; 63 | 64 | struct cap_event *event; 65 | 66 | event = bpf_ringbuf_reserve(&print_events, sizeof(struct cap_event), 0); 67 | if (!event) { 68 | return 0; 69 | } 70 | 71 | event->pid = pid; 72 | event->tgid = pid_tgid; 73 | event->cap = cap; 74 | event->uid = bpf_get_current_uid_gid(); 75 | event->mntnsid = mntns_id; 76 | event->cap_opt = cap_opt; 77 | bpf_get_current_comm(&event->task, sizeof(event->task)); 78 | 79 | bpf_ringbuf_submit(event, 0); 80 | 81 | return 0; 82 | } 83 | 84 | char LICENSE[] SEC("license") = "GPL"; 85 | -------------------------------------------------------------------------------- /examples/exitsnoop/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The exitsnoop example is heavily based on the [exitsnoop program in BCC's libbpf-tools](https://github.com/iovisor/bcc/blob/master/libbpf-tools/exitsnoop.bpf.c), which is itself based on the original [BCC exitsnoop](https://github.com/iovisor/bcc/blob/master/tools/exitsnoop.py). 4 | 5 | This eBPF program will trace all process termination (exit, fatal signals). 6 | 7 | # Usage 8 | 9 | To see all the syscalls in your environment, you can run your image without filters: 10 | 11 | ```console 12 | bee run ghcr.io/solo-io/bumblebee/exitsnoop:$(bee version) 13 | ``` 14 | 15 | You can also try out the filtering capability by referencing fields in your BPF map: 16 | 17 | ```c 18 | struct event { 19 | __u64 start_time; 20 | __u64 exit_time; 21 | __u32 pid; 22 | __u32 tid; 23 | __u32 ppid; 24 | __u32 sig; 25 | int exit_code; 26 | char comm[TASK_COMM_LEN]; 27 | }; 28 | ``` 29 | 30 | For example, to filter for all the syscalls where the `comm` is `bash`, you can use: 31 | 32 | ```console 33 | bee run -f="print_exits,comm,bash" ghcr.io/solo-io/bumblebee/exitsnoop:$(bee version) 34 | ``` 35 | 36 | # Prometheus integration 37 | 38 | Let's say, you want to visualize the rate of such syscalls in your Prometheus stack, or want to alert on certain syscalls. 39 | 40 | You can modify your `print_exits` map to generate a `counter` from your exit() calls: 41 | 42 | ```c 43 | struct { 44 | __uint(type, BPF_MAP_TYPE_RINGBUF); 45 | __uint(max_entries, 1 << 24); 46 | __type(value, struct event); 47 | } counter_exits SEC(".maps"); 48 | ``` 49 | 50 | This will generate Prometheus metrics like this: 51 | 52 | ```console 53 | # HELP ebpf_solo_io_counter_exits 54 | # TYPE ebpf_solo_io_counter_exits counter 55 | ebpf_solo_io_counter_exits{comm="infocmp",exit_code="0",exit_time="2396600383928",pid="16234",ppid="16221",sig="0",start_time="2396599269958",tid="16234"} 1 56 | ebpf_solo_io_counter_exits{comm="ip6tables",exit_code="0",exit_time="2402597588547",pid="16237",ppid="2115",sig="0",start_time="2402596148575",tid="16237"} 1 57 | ebpf_solo_io_counter_exits{comm="iptables",exit_code="0",exit_time="2397474999464",pid="16235",ppid="2115",sig="0",start_time="2397473028592",tid="16235"} 1 58 | ``` 59 | 60 | Note that some of the fields are not important in this usecase, and these can also overload your Prometheus instace, so if your use-case is only about Prometheus, you should consider removing these high cardinality fields from your map. 61 | `exit_time`, `start_time`, are fields that should be removed in this case. -------------------------------------------------------------------------------- /examples/exitsnoop/exitsnoop.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | /* Copyright (c) 2021 Hengqi Chen */ 3 | /* Adapted for `bee` from: https://github.com/iovisor/bcc/blob/master/libbpf-tools/exitsnoop.bpf.c */ 4 | #include 5 | #include 6 | #include 7 | #include "exitsnoop.h" 8 | 9 | //const volatile pid_t target_pid = 0; 10 | //const volatile bool trace_by_process = true; 11 | const volatile bool trace_failed_only = false; 12 | 13 | struct { 14 | __uint(type, BPF_MAP_TYPE_RINGBUF); 15 | __uint(max_entries, 1 << 24); 16 | __type(value, struct event); 17 | } print_exits SEC(".maps"); 18 | 19 | SEC("tracepoint/sched/sched_process_exit") 20 | int sched_process_exit(void *ctx) 21 | { 22 | __u64 pid_tgid = bpf_get_current_pid_tgid(); 23 | __u32 pid = pid_tgid >> 32; 24 | __u32 tid = (__u32)pid_tgid; 25 | int exit_code; 26 | struct task_struct *task; 27 | struct event event = {}; 28 | 29 | // if (target_pid && target_pid != pid) 30 | // return 0; 31 | // if (trace_by_process && pid != tid) 32 | // return 0; 33 | 34 | task = (struct task_struct *)bpf_get_current_task(); 35 | exit_code = BPF_CORE_READ(task, exit_code); 36 | if (trace_failed_only && exit_code == 0) 37 | return 0; 38 | 39 | event.start_time = BPF_CORE_READ(task, start_time); 40 | event.exit_time = bpf_ktime_get_ns(); 41 | event.pid = pid; 42 | event.tid = tid; 43 | event.ppid = BPF_CORE_READ(task, real_parent, tgid); 44 | event.sig = exit_code & 0xff; 45 | event.exit_code = exit_code >> 8; 46 | bpf_get_current_comm(event.comm, sizeof(event.comm)); 47 | 48 | struct event *ring_val; 49 | 50 | ring_val = bpf_ringbuf_reserve(&print_exits, sizeof(struct event), 0); 51 | if (!ring_val) { 52 | return 0; 53 | } 54 | 55 | memcpy(ring_val, &event, sizeof(struct event)); 56 | 57 | /* submit event to ringbuf for printing */ 58 | bpf_ringbuf_submit(ring_val, 0); 59 | 60 | return 0; 61 | } 62 | 63 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; -------------------------------------------------------------------------------- /examples/exitsnoop/exitsnoop.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __EXITSNOOP_H 3 | #define __EXITSNOOP_H 4 | 5 | #define TASK_COMM_LEN 16 6 | 7 | struct event { 8 | __u64 start_time; 9 | __u64 exit_time; 10 | __u32 pid; 11 | __u32 tid; 12 | __u32 ppid; 13 | __u32 sig; 14 | int exit_code; 15 | char comm[TASK_COMM_LEN]; 16 | }; 17 | 18 | #endif /* __EXITSNOOP_H */ -------------------------------------------------------------------------------- /examples/kprobetcp/handler.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "bpf/bpf_helpers.h" 3 | #include "bpf/bpf_core_read.h" 4 | #include "bpf/bpf_tracing.h" 5 | #include "solo_types.h" 6 | 7 | char __license[] SEC("license") = "Dual MIT/GPL"; 8 | 9 | struct event_t { 10 | u32 pid; 11 | u32 uid; 12 | u32 type; 13 | duration uptime; 14 | } __attribute__((packed)); 15 | 16 | struct { 17 | __uint(type, BPF_MAP_TYPE_RINGBUF); 18 | __uint(max_entries, 1 << 24); 19 | __type(value, struct event_t); 20 | } print_events SEC(".maps"); 21 | 22 | SEC("kprobe/tcp_v4_connect") 23 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 24 | { 25 | struct event_t *task_info; 26 | 27 | task_info = bpf_ringbuf_reserve(&print_events, sizeof(struct event_t), 0); 28 | if (!task_info) { 29 | return 0; 30 | } 31 | 32 | task_info->pid = bpf_get_current_pid_tgid(); 33 | task_info->uid = bpf_get_current_uid_gid(); 34 | task_info->uptime = bpf_ktime_get_ns(); 35 | 36 | bpf_ringbuf_submit(task_info, 0); 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /examples/nfsstats/nfsstats.c: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/iovisor/bcc/blob/master/libbpf-tools/fsslower.bpf.c 2 | // SPDX-License-Identifier: GPL-2.0 3 | 4 | #include "vmlinux.h" 5 | #include "bpf/bpf_helpers.h" 6 | #include "bpf/bpf_core_read.h" 7 | #include "bpf/bpf_tracing.h" 8 | #include "solo_types.h" 9 | 10 | // Example for tracing NFS operation file duration using histogram metrics 11 | 12 | char __license[] SEC("license") = "Dual MIT/GPL"; 13 | 14 | struct event { 15 | char fname[255]; 16 | char op; 17 | u64 le; 18 | }; 19 | 20 | struct event_start { 21 | u64 ts; 22 | struct file *fp; 23 | }; 24 | 25 | struct { 26 | __uint(type, BPF_MAP_TYPE_HASH); 27 | __uint(max_entries, 4096); 28 | __type(key, u32); 29 | __type(value, struct event_start); 30 | } start SEC(".maps"); 31 | 32 | struct { 33 | __uint(type, BPF_MAP_TYPE_RINGBUF); 34 | __uint(max_entries, 1 << 24); 35 | __type(value, struct event); 36 | } hist_nfs_op_time_us SEC(".maps"); 37 | 38 | static __always_inline int 39 | probe_entry(struct file *fp) 40 | { 41 | bpf_printk("nfs_file_read happened"); 42 | 43 | struct event_start evt = {}; 44 | 45 | u32 tgid = bpf_get_current_pid_tgid() >> 32; 46 | u64 ts = bpf_ktime_get_ns(); 47 | 48 | evt.ts = ts; 49 | evt.fp = fp; 50 | bpf_map_update_elem(&start, &tgid, &evt, 0); 51 | 52 | return 0; 53 | } 54 | 55 | static __always_inline int 56 | probe_exit(char op) { 57 | struct event evt = {}; 58 | struct file *fp; 59 | struct dentry *dentry; 60 | const __u8 *file_name; 61 | 62 | u32 tgid = bpf_get_current_pid_tgid() >> 32; 63 | struct event_start *rs; 64 | 65 | rs = bpf_map_lookup_elem(&start, &tgid); 66 | if (!rs) 67 | return 0; 68 | 69 | u64 ts = bpf_ktime_get_ns(); 70 | u64 duration = (ts - rs->ts) / 1000; 71 | 72 | bpf_printk("nfs operation duration: %lld", duration); 73 | 74 | evt.le = duration; 75 | evt.op = op; 76 | 77 | // decode filename 78 | fp = rs->fp; 79 | dentry = BPF_CORE_READ(fp, f_path.dentry); 80 | file_name = BPF_CORE_READ(dentry, d_name.name); 81 | bpf_probe_read_kernel_str(evt.fname, sizeof(evt.fname), file_name); 82 | bpf_printk("nfs op file_name: %s", evt.fname); 83 | 84 | struct event *ring_val; 85 | ring_val = bpf_ringbuf_reserve(&hist_nfs_op_time_us, sizeof(evt), 0); 86 | if (!ring_val) 87 | return 0; 88 | 89 | memcpy(ring_val, &evt, sizeof(evt)); 90 | bpf_ringbuf_submit(ring_val, 0); 91 | } 92 | 93 | SEC("kprobe/nfs_file_read") 94 | int BPF_KPROBE(nfs_file_read, struct kiocb *iocb) { 95 | bpf_printk("nfs_file_read happened"); 96 | struct file *fp = BPF_CORE_READ(iocb, ki_filp); 97 | return probe_entry(fp); 98 | } 99 | 100 | SEC("kretprobe/nfs_file_read") 101 | int BPF_KRETPROBE(nfs_file_read_ret, ssize_t ret) { 102 | bpf_printk("nfs_file_read returtned"); 103 | return probe_exit('r'); 104 | } 105 | 106 | SEC("kprobe/nfs_file_write") 107 | int BPF_KPROBE(nfs_file_write, struct kiocb *iocb) { 108 | bpf_printk("nfs_file_write happened"); 109 | struct file *fp = BPF_CORE_READ(iocb, ki_filp); 110 | return probe_entry(fp); 111 | } 112 | 113 | SEC("kretprobe/nfs_file_write") 114 | int BPF_KRETPROBE(nfs_file_write_ret, ssize_t ret) { 115 | bpf_printk("nfs_file_write returtned"); 116 | return probe_exit('w'); 117 | } 118 | 119 | SEC("kprobe/nfs_file_open") 120 | int BPF_KPROBE(nfs_file_open, struct inode *inode, struct file *fp) { 121 | bpf_printk("nfs_file_open happened"); 122 | return probe_entry(fp); 123 | } 124 | 125 | SEC("kretprobe/nfs_file_open") 126 | int BPF_KRETPROBE(nfs_file_open_ret, struct file *fp) { 127 | bpf_printk("nfs_file_open returtned"); 128 | return probe_exit('o'); 129 | } 130 | -------------------------------------------------------------------------------- /examples/oomkill/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The oomkill example is heavily based on the [oomkill program in BCC's libbpf-tools](https://github.com/iovisor/bcc/blob/master/libbpf-tools/oomkill.bpf.c), which is itself based on the original [BCC oomkill](https://github.com/iovisor/bcc/blob/master/tools/oomkill.py), created by Brendan Gregg. 4 | 5 | The script traces the kernel out-of-memory killer, and prints details about which process was killed. 6 | 7 | The main difference between the original program and this one, is that here, we are using a new BPF data structure, called [BPF ring buffer](https://www.kernel.org/doc/html/latest/bpf/ringbuf.html), while the original one is using BPF perf buffer. 8 | 9 | Due to this decision, this version can be only used starting from Linux 5.8 kernel version, however this data structure solves multiple shortcomings of perfbuf, e.g. event ordering, and memory utilization, and using it instead of perfbuf is considered the current best practice. 10 | 11 | # Usage 12 | 13 | To see all the oomkills in your environment, you can run your image without filters: 14 | 15 | ```console 16 | bee run ghcr.io/solo-io/bumblebee/oomkill:$(bee version) 17 | ``` 18 | 19 | You can also try out the filtering capability by referencing fields in your BPF map: 20 | 21 | ```c 22 | struct data_t { 23 | __u32 fpid; 24 | __u32 tpid; 25 | __u64 pages; 26 | char fcomm[TASK_COMM_LEN]; 27 | char tcomm[TASK_COMM_LEN]; 28 | }; 29 | ``` 30 | 31 | The fields `tcomm`, and `tpid` identify the process that was oomkilled, while `fcomm` and `fpid` are details about the process that was triggering the oomkiller. 32 | 33 | For example, to filter for all the oomkills where the oomkilled process is `bash`, you can use: 34 | 35 | ```console 36 | bee run -f="exits,tcomm,bash" ghcr.io/solo-io/bumblebee/oomkill:$(bee version) 37 | ``` 38 | 39 | # Prometheus integration 40 | 41 | Visualizing and alerting on oomkills is crucial, and Bumblebee can help you with that. 42 | 43 | Since we have the `counter_` prefix added to our `oomkills` map, exposing the oomkill events as a `counter` metric is enabled by default: 44 | 45 | ```c 46 | struct { 47 | __uint(type, BPF_MAP_TYPE_RINGBUF); 48 | __uint(max_entries, 1 << 24); 49 | __type(value, struct data_t); 50 | } counter_oomkills SEC(".maps"); 51 | ``` 52 | 53 | To disable this functionality, you can change the prefix to `print_`. 54 | -------------------------------------------------------------------------------- /examples/oomkill/oomkill.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | // Copyright (c) 2022 Jingxiang Zeng 3 | /* Adapted for `bee` from: https://github.com/iovisor/bcc/blob/master/libbpf-tools/oomkill.bpf.c */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define TASK_COMM_LEN 16 10 | 11 | struct data_t { 12 | __u32 fpid; 13 | __u32 tpid; 14 | __u64 pages; 15 | char fcomm[TASK_COMM_LEN]; 16 | char tcomm[TASK_COMM_LEN]; 17 | }; 18 | 19 | struct { 20 | __uint(type, BPF_MAP_TYPE_RINGBUF); 21 | __uint(max_entries, 1 << 24); 22 | __type(value, struct data_t); 23 | } counter_oomkills SEC(".maps"); 24 | 25 | SEC("kprobe/oom_kill_process") 26 | int BPF_KPROBE(oom_kill_process, struct oom_control *oc, const char *message) 27 | { 28 | struct data_t *e; 29 | 30 | e = bpf_ringbuf_reserve(&counter_oomkills, sizeof(struct data_t), 0); 31 | if (!e) { 32 | return 0; 33 | } 34 | 35 | e->tpid = BPF_CORE_READ(oc, chosen, tgid); 36 | bpf_get_current_comm(&e->fcomm, TASK_COMM_LEN); 37 | 38 | e->fpid = bpf_get_current_pid_tgid() >> 32; 39 | e->pages = BPF_CORE_READ(oc, totalpages); 40 | bpf_probe_read_kernel(&e->tcomm, sizeof(e->tcomm), BPF_CORE_READ(oc, chosen, comm)); 41 | 42 | bpf_ringbuf_submit(e, 0); 43 | 44 | return 0; 45 | } 46 | 47 | char LICENSE[] SEC("license") = "GPL"; 48 | -------------------------------------------------------------------------------- /examples/opensnoop/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The `opensnoop` example is heavily based on the [`opensnoop` program in the BCC's libbpf-tools](https://github.com/iovisor/bcc/blob/master/libbpf-tools/opensnoop.c), which is itself based on the original [BCC `opensnoop`](https://github.com/iovisor/bcc/blob/master/tools/opensnoop.py) created by Brendan Gregg. 4 | 5 | This eBPF program will trace all `open()` syscalls by attaching to tracepoints in the kernel and sending data related to `open()` call (e.g. which file name is being opened, which command is executing the `open()`, etc.) to a map which will then be streamed to the `bee` runner. 6 | 7 | However, as you can probably imagine, there are quite a lot of `open()` syscalls happening on a system at any given time, so in order to make this info more digestable, `bee` provides support for filtering the output by regular expressions applied to the various fields in the data. Eventually we plan on exposing filters in the eBPF program itself through the `bee run` command as well. 8 | 9 | To see this in action, you can run `opensnoop` without any filter, and you should see a fairly active stream of data: 10 | ``` 11 | bee run ghcr.io/solo-io/bumblebee/opensnoop:$(bee version) 12 | ``` 13 | 14 | Now we can start paring this down with the filtering capability. Let's run `opensnoop` with a filter applied, such as: 15 | ``` 16 | bee run -f="print_events,comm,node" ghcr.io/solo-io/bumblebee/opensnoop:$(bee version) 17 | ``` 18 | This command will run the `opensnoop` program (pulled from our GitHub container registry) and filter the entries in the `print_events` map to only entries that have a `comm` value of `node`. In other words, we will be viewing `open()` syscalls that have been initiated by the `node` command. 19 | 20 | Note that the last field is a regex, so you can get more creative than simply adding the command name if you want. 21 | 22 | As another example, if you wanted to filter for `open()`s by the root user (userid `0`) you could do: 23 | ``` 24 | bee run -f="print_events,uid,0" ghcr.io/solo-io/bumblebee/opensnoop:$(bee version) 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/opensnoop/opensnoop.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | // Copyright (c) 2019 Facebook 3 | // Copyright (c) 2020 Netflix 4 | // Adapted for `bee` from: https://github.com/iovisor/bcc/blob/master/libbpf-tools/opensnoop.bpf.c 5 | #include 6 | #include 7 | #include "opensnoop.h" 8 | 9 | #define TASK_RUNNING 0 10 | 11 | // const volatile __u64 min_us = 0; 12 | // const volatile pid_t targ_pid = 0; 13 | // const volatile pid_t targ_tgid = 0; 14 | // const volatile uid_t targ_uid = 0; 15 | // const volatile bool targ_failed = false; 16 | 17 | struct { 18 | __uint(type, BPF_MAP_TYPE_HASH); 19 | __uint(max_entries, 10240); 20 | __type(key, u32); 21 | __type(value, struct args_t); 22 | } start SEC(".maps"); 23 | 24 | struct { 25 | __uint(type, BPF_MAP_TYPE_RINGBUF); 26 | __uint(max_entries, 1 << 24); 27 | __type(value, struct event); 28 | } print_events SEC(".maps"); 29 | 30 | // static __always_inline bool valid_uid(uid_t uid) { 31 | // return uid != INVALID_UID; 32 | // } 33 | 34 | // static __always_inline 35 | // bool trace_allowed(u32 tgid, u32 pid) 36 | // { 37 | // u32 uid; 38 | 39 | // /* filters */ 40 | // if (targ_tgid && targ_tgid != tgid) 41 | // return false; 42 | // if (targ_pid && targ_pid != pid) 43 | // return false; 44 | // if (valid_uid(targ_uid)) { 45 | // uid = (u32)bpf_get_current_uid_gid(); 46 | // if (targ_uid != uid) { 47 | // return false; 48 | // } 49 | // } 50 | // return true; 51 | // } 52 | 53 | SEC("tracepoint/syscalls/sys_enter_open") 54 | int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx) 55 | { 56 | u64 id = bpf_get_current_pid_tgid(); 57 | /* use kernel terminology here for tgid/pid: */ 58 | // u32 tgid = id >> 32; 59 | u32 pid = id; 60 | 61 | /* store arg info for later lookup */ 62 | // if (trace_allowed(tgid, pid)) { 63 | struct args_t args = {}; 64 | args.fname = (const char *)ctx->args[0]; 65 | args.flags = (int)ctx->args[1]; 66 | bpf_map_update_elem(&start, &pid, &args, 0); 67 | // } 68 | return 0; 69 | } 70 | 71 | SEC("tracepoint/syscalls/sys_enter_openat") 72 | int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx) 73 | { 74 | u64 id = bpf_get_current_pid_tgid(); 75 | /* use kernel terminology here for tgid/pid: */ 76 | u32 tgid = id >> 32; 77 | u32 pid = id; 78 | 79 | /* store arg info for later lookup */ 80 | // if (trace_allowed(tgid, pid)) { 81 | struct args_t args = {}; 82 | args.fname = (const char *)ctx->args[1]; 83 | args.flags = (int)ctx->args[2]; 84 | bpf_map_update_elem(&start, &pid, &args, 0); 85 | // } 86 | return 0; 87 | } 88 | 89 | static __always_inline 90 | int trace_exit(struct trace_event_raw_sys_exit* ctx) 91 | { 92 | struct event event = {}; 93 | struct args_t *ap; 94 | int ret; 95 | u32 pid = bpf_get_current_pid_tgid(); 96 | 97 | ap = bpf_map_lookup_elem(&start, &pid); 98 | if (!ap) 99 | return 0; /* missed entry */ 100 | ret = ctx->ret; 101 | // if (targ_failed && ret >= 0) 102 | // goto cleanup; /* want failed only */ 103 | 104 | /* event data */ 105 | event.pid = bpf_get_current_pid_tgid() >> 32; 106 | event.uid = bpf_get_current_uid_gid(); 107 | bpf_get_current_comm(&event.comm, sizeof(event.comm)); 108 | bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname); 109 | event.flags = ap->flags; 110 | event.ret = ret; 111 | 112 | struct event *ring_val; 113 | 114 | ring_val = bpf_ringbuf_reserve(&print_events, sizeof(struct event), 0); 115 | if (!ring_val) { 116 | return 0; 117 | } 118 | 119 | memcpy(ring_val, &event, sizeof(struct event)); 120 | 121 | /* submit event to ringbuf for printing */ 122 | bpf_ringbuf_submit(ring_val, 0); 123 | 124 | // cleanup: 125 | bpf_map_delete_elem(&start, &pid); 126 | return 0; 127 | } 128 | 129 | SEC("tracepoint/syscalls/sys_exit_open") 130 | int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx) 131 | { 132 | return trace_exit(ctx); 133 | } 134 | 135 | SEC("tracepoint/syscalls/sys_exit_openat") 136 | int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx) 137 | { 138 | return trace_exit(ctx); 139 | } 140 | 141 | char LICENSE[] SEC("license") = "GPL"; 142 | -------------------------------------------------------------------------------- /examples/opensnoop/opensnoop.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __OPENSNOOP_H 3 | #define __OPENSNOOP_H 4 | 5 | #define TASK_COMM_LEN 16 6 | #define NAME_MAX 255 7 | #define INVALID_UID ((uid_t)-1) 8 | 9 | struct args_t { 10 | const char *fname; 11 | int flags; 12 | }; 13 | 14 | struct event { 15 | /* user terminology for pid: */ 16 | __u64 ts; 17 | pid_t pid; 18 | uid_t uid; 19 | int ret; 20 | int flags; 21 | char comm[TASK_COMM_LEN]; 22 | char fname[NAME_MAX]; 23 | }; 24 | 25 | #endif /* __OPENSNOOP_H */ 26 | -------------------------------------------------------------------------------- /examples/tcpconnect/tcpconnect.c: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnect.c 2 | #include "vmlinux.h" 3 | #include "solo_types.h" 4 | #include "bpf/bpf_helpers.h" 5 | #include "bpf/bpf_core_read.h" 6 | #include "bpf/bpf_tracing.h" 7 | 8 | char __license[] SEC("license") = "Dual MIT/GPL"; 9 | 10 | struct dimensions_t { 11 | ipv4_addr saddr; 12 | ipv4_addr daddr; 13 | } __attribute__((packed)); 14 | 15 | struct { 16 | __uint(type, BPF_MAP_TYPE_HASH); 17 | __uint(max_entries, 8192); 18 | __type(key, u32); 19 | __type(value, struct sock *); 20 | __uint(map_flags, BPF_F_NO_PREALLOC); 21 | } sockets SEC(".maps"); 22 | 23 | struct { 24 | __uint(type, BPF_MAP_TYPE_HASH); 25 | __uint(max_entries, 8192); 26 | __type(key, struct dimensions_t); 27 | __type(value, u64); 28 | } counter_events_hash SEC(".maps"); 29 | 30 | struct { 31 | __uint(type, BPF_MAP_TYPE_RINGBUF); 32 | __uint(max_entries, 1 << 24); 33 | __type(value, struct dimensions_t); 34 | } counter_events_ring SEC(".maps"); 35 | 36 | static __always_inline int 37 | enter_tcp_connect(struct pt_regs *ctx, struct sock *sk) 38 | { 39 | __u64 pid_tgid = bpf_get_current_pid_tgid(); 40 | __u32 tid = pid_tgid; 41 | bpf_printk("enter called"); 42 | 43 | bpf_printk("enter: setting sk for tid: %u", tid); 44 | bpf_map_update_elem(&sockets, &tid, &sk, 0); 45 | return 0; 46 | } 47 | 48 | static __always_inline int 49 | exit_tcp_connect(struct pt_regs *ctx, int ret) 50 | { 51 | __u64 pid_tgid = bpf_get_current_pid_tgid(); 52 | __u32 tid = pid_tgid; 53 | struct sock **skpp; 54 | struct sock *sk; 55 | 56 | __u32 saddr; 57 | __u32 daddr; 58 | u64 val; 59 | u64 *valp; 60 | struct dimensions_t hash_key = {}; 61 | 62 | bpf_printk("exit: getting sk for tid: '%u', ret is: '%d'", tid, ret); 63 | skpp = bpf_map_lookup_elem(&sockets, &tid); 64 | if (!skpp) { 65 | bpf_printk("exit: no pointer for tid, returning: %u", tid); 66 | return 0; 67 | } 68 | sk = *skpp; 69 | 70 | bpf_printk("exit: found sk for tid: %u", tid); 71 | BPF_CORE_READ_INTO(&saddr, sk, __sk_common.skc_rcv_saddr); 72 | BPF_CORE_READ_INTO(&daddr, sk, __sk_common.skc_daddr); 73 | hash_key.saddr = saddr; 74 | hash_key.daddr = daddr; 75 | 76 | // Set Hash map 77 | valp = bpf_map_lookup_elem(&counter_events_hash, &hash_key); 78 | if (!valp) { 79 | bpf_printk("no entry for {saddr: %u, daddr: %u}", hash_key.saddr, hash_key.daddr); 80 | val = 1; 81 | } 82 | else { 83 | bpf_printk("found existing value '%llu' for {saddr: %u, daddr: %u}", *valp, hash_key.saddr, hash_key.daddr); 84 | val = *valp + 1; 85 | } 86 | bpf_map_update_elem(&counter_events_hash, &hash_key, &val, 0); 87 | bpf_map_delete_elem(&sockets, &tid); 88 | 89 | // Set Ringbuffer 90 | struct dimensions_t *ring_val; 91 | 92 | ring_val = bpf_ringbuf_reserve(&counter_events_ring, sizeof(struct dimensions_t), 0); 93 | if (!ring_val) { 94 | return 0; 95 | } 96 | 97 | ring_val->saddr = saddr; 98 | ring_val->daddr = daddr; 99 | 100 | bpf_ringbuf_submit(ring_val, 0); 101 | 102 | return 0; 103 | } 104 | 105 | SEC("kprobe/tcp_v4_connect") 106 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 107 | { 108 | return enter_tcp_connect(ctx, sk); 109 | } 110 | 111 | SEC("kretprobe/tcp_v4_connect") 112 | int BPF_KRETPROBE(tcp_v4_connect_ret, int ret) 113 | { 114 | return exit_tcp_connect(ctx, ret); 115 | } 116 | -------------------------------------------------------------------------------- /examples/tcpconnlat/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The tcpconnlat example is heavily based on the [tcpconnlat program in BCC's libbpf-tools](https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnlat.bpf.c), which is itself based on the original [BCC tcpconnlat](https://github.com/iovisor/bcc/blob/master/tools/tcpconnlat.py). 4 | 5 | This eBPF program will trace all TCP active connection latencies. 6 | 7 | # Usage 8 | 9 | To see all the connections in your environment, you can run your image without filters: 10 | 11 | ```console 12 | bee run ghcr.io/solo-io/bumblebee/tcpconnlat:$(bee version) 13 | ``` 14 | 15 | You can also try out the filtering capability by referencing fields in your BPF map: 16 | 17 | ```c 18 | struct event { 19 | ipv4_addr saddr_v4; 20 | ipv4_addr daddr_v4; 21 | char comm[TASK_COMM_LEN]; 22 | __u64 delta_us; 23 | __u64 ts_us; 24 | __u32 tgid; 25 | int af; 26 | }; 27 | ``` 28 | 29 | For example, to filter for all active connections where the `daadr_v4` is `8.8.8.8`, you can use: 30 | 31 | ```console 32 | bee run -f="print_events,daddr_v4,8.8.8.8" ghcr.io/solo-io/bumblebee/tcpconnlat:$(bee version) 33 | ``` 34 | 35 | Result: 36 | 37 | ```console 38 | af comm daddr_v4 delta_us saddr_v4 tgid ts_us 39 | 2 telnet 8.8.8.8 1220 10.132.0.48 15173 5149693413 40 | ``` 41 | 42 | # Prometheus integration 43 | 44 | Let's say, you want to visualize the latencies in your Prometheus stack, or want to alert on certain limits. 45 | 46 | > Note that BumbleBee currently only supports counter and gauge metric types, so as of now, you cannot expose latency metrics as histograms. The support for histogram is on our [roadmap](https://github.com/solo-io/bumblebee/blob/main/ROADMAP.md). 47 | 48 | > Also note that currently BumbleBee is exposing metrics for all the members of the struct describing the map as labels. As `ts_us` is there as a timestamp, the cardinality will explode quite soon, so **generate Prometheus metrics only in a lab or a very low traffic environment**. 49 | 50 | You can modify your `print_events` map to generate a `counter` from your active connections: 51 | 52 | ```c 53 | struct { 54 | __uint(type, BPF_MAP_TYPE_RINGBUF); 55 | __uint(max_entries, 1 << 24); 56 | __type(value, struct event); 57 | } counter_events SEC(".maps"); 58 | ``` 59 | 60 | This will generate Prometheus metrics like this: 61 | 62 | ```console 63 | # HELP ebpf_solo_io_counter_events 64 | # TYPE ebpf_solo_io_counter_events counter 65 | ebpf_solo_io_counter_events{af="2",comm="coredns",daddr_v4="127.0.0.1",delta_us="44",saddr_v4="127.0.0.1",tgid="4508",ts_us="5914339221"} 1 66 | ebpf_solo_io_counter_events{af="2",comm="coredns",daddr_v4="127.0.0.1",delta_us="46",saddr_v4="127.0.0.1",tgid="4508",ts_us="5910339887"} 1 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/tcpconnlat/tcpconnlat.c: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnlat.bpf.c 2 | // SPDX-License-Identifier: GPL-2.0 3 | // Copyright (c) 2020 Wenbo Zhang 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "solo_types.h" 10 | 11 | #define TASK_COMM_LEN 16 12 | 13 | struct event { 14 | ipv4_addr saddr_v4; 15 | ipv4_addr daddr_v4; 16 | char comm[TASK_COMM_LEN]; 17 | __u64 delta_us; 18 | __u64 ts_us; 19 | __u32 tgid; 20 | int af; 21 | }; 22 | 23 | struct piddata { 24 | char comm[TASK_COMM_LEN]; 25 | u64 ts; 26 | u32 tgid; 27 | }; 28 | 29 | //Commenting these and the corresponding logic out until we have support for kernel-side filtering. 30 | //const volatile __u64 targ_min_us = 0; 31 | //const volatile pid_t targ_tgid = 0; 32 | 33 | struct { 34 | __uint(type, BPF_MAP_TYPE_HASH); 35 | __uint(max_entries, 4096); 36 | __type(key, struct sock *); 37 | __type(value, struct piddata); 38 | } start SEC(".maps"); 39 | 40 | struct { 41 | __uint(type, BPF_MAP_TYPE_RINGBUF); 42 | __uint(max_entries, 1 << 24); 43 | __type(value, struct event); 44 | } print_events SEC(".maps"); 45 | 46 | static int trace_connect(struct sock *sk) 47 | { 48 | u32 tgid = bpf_get_current_pid_tgid() >> 32; 49 | struct piddata piddata = {}; 50 | 51 | // if (targ_tgid && targ_tgid != tgid) 52 | // return 0; 53 | 54 | bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm)); 55 | piddata.ts = bpf_ktime_get_ns(); 56 | piddata.tgid = tgid; 57 | bpf_map_update_elem(&start, &sk, &piddata, 0); 58 | return 0; 59 | } 60 | 61 | static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk) 62 | { 63 | struct piddata *piddatap; 64 | struct event event = {}; 65 | s64 delta; 66 | u64 ts; 67 | 68 | if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT) 69 | return 0; 70 | 71 | piddatap = bpf_map_lookup_elem(&start, &sk); 72 | if (!piddatap) 73 | return 0; 74 | 75 | ts = bpf_ktime_get_ns(); 76 | delta = (s64)(ts - piddatap->ts); 77 | if (delta < 0) { 78 | bpf_map_delete_elem(&start, &sk); 79 | return 0; 80 | } 81 | 82 | event.delta_us = delta / 1000U; 83 | 84 | // if (targ_min_us && event.delta_us < targ_min_us) { 85 | // bpf_map_delete_elem(&start, &sk); 86 | // return 0; 87 | // } 88 | 89 | __builtin_memcpy(&event.comm, piddatap->comm, 90 | sizeof(event.comm)); 91 | event.ts_us = ts / 1000; 92 | event.tgid = piddatap->tgid; 93 | event.af = BPF_CORE_READ(sk, __sk_common.skc_family); 94 | event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); 95 | event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr); 96 | 97 | struct event *ring_val; 98 | 99 | ring_val = bpf_ringbuf_reserve(&print_events, sizeof(struct event), 0); 100 | if (!ring_val) { 101 | return 0; 102 | } 103 | 104 | memcpy(ring_val, &event, sizeof(struct event)); 105 | 106 | bpf_ringbuf_submit(ring_val, 0); 107 | 108 | return 0; 109 | } 110 | 111 | SEC("kprobe/tcp_v4_connect") 112 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 113 | { 114 | return trace_connect(sk); 115 | } 116 | 117 | SEC("kprobe/tcp_rcv_state_process") 118 | int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk) 119 | { 120 | return handle_tcp_rcv_state_process(ctx, sk); 121 | } 122 | 123 | char LICENSE[] SEC("license") = "GPL"; 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/solo-io/bumblebee 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.10.0 7 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 8 | github.com/manifoldco/promptui v0.9.0 9 | github.com/mitchellh/hashstructure/v2 v2.0.2 10 | github.com/onsi/ginkgo v1.16.5 11 | github.com/onsi/gomega v1.16.0 12 | github.com/opencontainers/go-digest v1.0.0 13 | github.com/opencontainers/image-spec v1.0.2 14 | github.com/prometheus/client_golang v1.11.0 15 | github.com/pterm/pterm v0.12.33 16 | github.com/rivo/tview v0.0.0-20211109175620-badfa0f0b301 17 | github.com/solo-io/go-utils v0.21.24 18 | github.com/spf13/cobra v1.2.1 19 | github.com/spf13/pflag v1.0.5 20 | go.uber.org/zap v1.17.0 21 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 22 | oras.land/oras-go v1.0.0 23 | ) 24 | 25 | require ( 26 | github.com/docker/cli v20.10.11+incompatible 27 | github.com/docker/docker v20.10.11+incompatible 28 | github.com/pkg/errors v0.9.1 29 | ) 30 | 31 | require ( 32 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 33 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 34 | github.com/atomicgo/cursor v0.0.1 // indirect 35 | github.com/avast/retry-go v2.2.0+incompatible // indirect 36 | github.com/beorn7/perks v1.0.1 // indirect 37 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 38 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 39 | github.com/containerd/containerd v1.5.9 // indirect 40 | github.com/docker/distribution v2.7.1+incompatible // indirect 41 | github.com/docker/docker-credential-helpers v0.6.4 // indirect 42 | github.com/docker/go-connections v0.4.0 // indirect 43 | github.com/docker/go-metrics v0.0.1 // indirect 44 | github.com/docker/go-units v0.4.0 // indirect 45 | github.com/fsnotify/fsnotify v1.4.9 // indirect 46 | github.com/gdamore/encoding v1.0.0 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/protobuf v1.5.2 // indirect 49 | github.com/google/go-github/v32 v32.0.0 // indirect 50 | github.com/google/go-querystring v1.0.0 // indirect 51 | github.com/gookit/color v1.4.2 // indirect 52 | github.com/gorilla/mux v1.8.0 // indirect 53 | github.com/imroc/req v0.3.0 // indirect 54 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 55 | github.com/k0kubun/pp v2.3.0+incompatible // indirect 56 | github.com/klauspost/compress v1.11.13 // indirect 57 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 58 | github.com/mattn/go-colorable v0.0.9 // indirect 59 | github.com/mattn/go-isatty v0.0.4 // indirect 60 | github.com/mattn/go-runewidth v0.0.13 // indirect 61 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 62 | github.com/moby/locker v1.0.1 // indirect 63 | github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect 64 | github.com/morikuni/aec v1.0.0 // indirect 65 | github.com/nxadm/tail v1.4.8 // indirect 66 | github.com/pelletier/go-toml v1.9.3 // indirect 67 | github.com/prometheus/client_model v0.2.0 // indirect 68 | github.com/prometheus/common v0.26.0 // indirect 69 | github.com/prometheus/procfs v0.6.0 // indirect 70 | github.com/rivo/uniseg v0.2.0 // indirect 71 | github.com/rotisserie/eris v0.1.1 // indirect 72 | github.com/sirupsen/logrus v1.8.1 // indirect 73 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 74 | go.uber.org/atomic v1.7.0 // indirect 75 | go.uber.org/multierr v1.6.0 // indirect 76 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect 77 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 78 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect 79 | golang.org/x/sys v0.2.0 // indirect 80 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 81 | golang.org/x/text v0.3.6 // indirect 82 | google.golang.org/appengine v1.6.7 // indirect 83 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 84 | google.golang.org/grpc v1.38.0 // indirect 85 | google.golang.org/protobuf v1.27.1 // indirect 86 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 87 | gopkg.in/yaml.v2 v2.4.0 // indirect 88 | ) 89 | -------------------------------------------------------------------------------- /install_cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | if [ -x "$(command -v python3)" ]; then 6 | alias any_python='python3' 7 | elif [ -x "$(command -v python)" ]; then 8 | alias any_python='python' 9 | elif [ -x "$(command -v python2)" ]; then 10 | alias any_python='python2' 11 | else 12 | echo Python 2 or 3 is required to install bee 13 | exit 1 14 | fi 15 | 16 | if [ -z "${BUMBLEBEE_VERSION:-}" ]; then 17 | BUMBLEBEE_VERSIONS=$(curl -sH"Accept: application/vnd.github.v3+json" https://api.github.com/repos/solo-io/bumblebee/releases | any_python -c "import sys; from distutils.version import StrictVersion, LooseVersion; from json import loads as l; releases = l(sys.stdin.read()); releases = [release['tag_name'] for release in releases]; filtered_releases = list(filter(lambda release_string: len(release_string) > 0 and StrictVersion.version_re.match(release_string[1:]) != None, releases)); filtered_releases.sort(key=LooseVersion, reverse=True); print('\n'.join(filtered_releases))") 18 | else 19 | BUMBLEBEE_VERSIONS="${BUMBLEBEE_VERSION}" 20 | fi 21 | 22 | if [ "$(uname -s)" = "Linux" ]; then 23 | OS=linux 24 | else 25 | echo Only linux is currently supported 26 | exit 1 27 | fi 28 | 29 | for bumblebee_version in $BUMBLEBEE_VERSIONS; do 30 | 31 | tmp=$(mktemp -d /tmp/ebpf.XXXXXX) 32 | filename="bee-${OS}-amd64" 33 | url="https://github.com/solo-io/bumblebee/releases/download/${bumblebee_version}/${filename}" 34 | 35 | if curl -f ${url} >/dev/null 2>&1; then 36 | echo "Attempting to download bee version ${bumblebee_version}" 37 | else 38 | continue 39 | fi 40 | 41 | ( 42 | cd "$tmp" 43 | 44 | echo "Downloading ${filename}..." 45 | 46 | SHA=$(curl -sL "${url}.sha256" | cut -d' ' -f1) 47 | curl -sLO "${url}" 48 | echo "Download complete!, validating checksum..." 49 | checksum=$(openssl dgst -sha256 "${filename}" | awk '{ print $2 }') 50 | if [ "$checksum" != "$SHA" ]; then 51 | echo "Checksum validation failed." >&2 52 | exit 1 53 | fi 54 | echo "Checksum valid." 55 | ) 56 | 57 | ( 58 | cd "$HOME" 59 | mkdir -p ".bumblebee/bin" 60 | mv "${tmp}/${filename}" ".bumblebee/bin/bee" 61 | chmod +x ".bumblebee/bin/bee" 62 | ) 63 | 64 | rm -r "$tmp" 65 | 66 | echo "bee was successfully installed 🎉" 67 | echo "" 68 | echo "Add the bumblebee CLI to your path with:" 69 | echo " export PATH=\$HOME/.bumblebee/bin:\$PATH" 70 | echo "" 71 | echo "Now run:" 72 | echo " bee init # Initialize simple eBPF program to run with bee" 73 | echo "Please see visit the bumblebee website for more info: https://github.com/solo-io/bumblebee" 74 | exit 0 75 | done 76 | 77 | echo "No versions of bee found." 78 | exit 1 79 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/cli/app.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | dockercliconfig "github.com/docker/cli/cli/config" 7 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/build" 8 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/describe" 9 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/initialize" 10 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/list" 11 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/login" 12 | package_cmd "github.com/solo-io/bumblebee/pkg/cli/internal/commands/package" 13 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/pull" 14 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/push" 15 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/run" 16 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/tag" 17 | "github.com/solo-io/bumblebee/pkg/cli/internal/commands/version" 18 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | func Bee() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "bee", 25 | } 26 | opts := options.NewGeneralOptions(cmd.PersistentFlags()) 27 | 28 | cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { 29 | if opts.AuthOptions.CredentialsFiles == nil { 30 | // use config file first first and then dockers, the enables: 31 | // - the first one will be used for writing (i.e. in login) 32 | // but users can also re-use their existing credentials from docker login. 33 | opts.AuthOptions.CredentialsFiles = []string{ 34 | filepath.Join(opts.ConfigDir, dockercliconfig.ConfigFileName), 35 | filepath.Join(dockercliconfig.Dir(), dockercliconfig.ConfigFileName), 36 | } 37 | } 38 | } 39 | 40 | cmd.AddCommand( 41 | build.Command(opts), 42 | package_cmd.Command(opts), 43 | run.Command(opts), 44 | initialize.Command(), 45 | push.Command(opts), 46 | pull.Command(opts), 47 | list.Command(opts), 48 | tag.Command(opts), 49 | describe.Command(opts), 50 | login.Command(opts), 51 | version.Command(opts), 52 | ) 53 | return cmd 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 14 | "github.com/pterm/pterm" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/pflag" 17 | "oras.land/oras-go/pkg/content" 18 | 19 | "github.com/solo-io/bumblebee/builder" 20 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 21 | "github.com/solo-io/bumblebee/pkg/internal/version" 22 | "github.com/solo-io/bumblebee/pkg/spec" 23 | ) 24 | 25 | type buildOptions struct { 26 | BuildImage string 27 | Builder string 28 | OutputFile string 29 | Local bool 30 | BinaryOnly bool 31 | CFlags []string 32 | BuildScript string 33 | BuildScriptOutput bool 34 | 35 | general *options.GeneralOptions 36 | } 37 | 38 | func (opts *buildOptions) validate() error { 39 | if !opts.Local { 40 | if opts.BuildScript != "" { 41 | fmt.Println("ignoring specified build script for docker build") 42 | } 43 | if opts.BuildScriptOutput { 44 | return fmt.Errorf("cannot write build script output for docker build") 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func addToFlags(flags *pflag.FlagSet, opts *buildOptions) { 52 | flags.StringVarP(&opts.BuildImage, "build-image", "i", fmt.Sprintf("ghcr.io/solo-io/bumblebee/builder:%s", version.Version), "Build image to use when compiling BPF program") 53 | flags.StringVarP(&opts.Builder, "builder", "b", "docker", "Executable to use for docker build command, default: `docker`") 54 | flags.StringVarP(&opts.OutputFile, "output-file", "o", "", "Output file for BPF ELF. If left blank will default to ") 55 | flags.BoolVarP(&opts.Local, "local", "l", false, "Build the output binary using local tools") 56 | flags.StringVar(&opts.BuildScript, "build-script", "", "Optional path to a build script for building BPF program locally") 57 | flags.BoolVar(&opts.BuildScriptOutput, "build-script-out", false, "Print local script bee will use to build the BPF program") 58 | flags.BoolVar(&opts.BinaryOnly, "binary-only", false, "Only create output binary and do not package it into an OCI image") 59 | flags.StringArrayVar(&opts.CFlags, "cflags", nil, "cflags to be used when compiling the BPF program, passed as environment variable 'CFLAGS'") 60 | } 61 | 62 | func Command(opts *options.GeneralOptions) *cobra.Command { 63 | buildOpts := &buildOptions{ 64 | general: opts, 65 | } 66 | cmd := &cobra.Command{ 67 | Use: "build INPUT_FILE REGISTRY_REF", 68 | Short: "Build a BPF program, and save it to an OCI image representation.", 69 | Long: ` 70 | The bee build command has 2 main parts 71 | 1. Compiling the BPF C program using clang. 72 | 2. Saving the compiled program in the OCI format. 73 | 74 | By default building is done in a docker container, however, this can be switched to local by adding the local flag: 75 | $ build INPUT_FILE REGISTRY_REF --local 76 | 77 | If you would prefer to build only the object file, you can include the '--binary-only' flag, 78 | in which case you do not need to specify a registry: 79 | $ build INPUT_FILE --binary-only 80 | 81 | Examples: 82 | You can specify multiple cflags with either a space separated string, or with multiple instances: 83 | $ build INPUT_FILE REGISTRY_REF --cflags="-DDEBUG -DDEBUG2" 84 | $ build INPUT_FILE REGISTRY_REF --cflags=-DDEBUG --cflags=-DDEBUG2 85 | 86 | You can specify your own build script for building the BPF program locally. 87 | the input file will be passed as argument '$1' and the output filename as '$2'. 88 | Use the '--build-script-out' flag to see the default build script bee uses: 89 | $ build INPUT_FILE REGISTRY_REF --local --build-script-out 90 | $ build INPUT_FILE REGISTRY_REF --local --build-script=build.sh 91 | `, 92 | Args: cobra.RangeArgs(1, 2), 93 | RunE: func(cmd *cobra.Command, args []string) error { 94 | return build(cmd.Context(), args, buildOpts) 95 | }, 96 | SilenceUsage: true, // Usage on error is bad 97 | } 98 | 99 | cmd.OutOrStdout() 100 | 101 | // Init flags 102 | addToFlags(cmd.PersistentFlags(), buildOpts) 103 | 104 | return cmd 105 | } 106 | 107 | func build(ctx context.Context, args []string, opts *buildOptions) error { 108 | if err := opts.validate(); err != nil { 109 | return err 110 | } 111 | 112 | inputFile := args[0] 113 | outputFile := opts.OutputFile 114 | 115 | var outputFd *os.File 116 | if outputFile == "" { 117 | ext := filepath.Ext(inputFile) 118 | 119 | filePath := strings.TrimSuffix(inputFile, ext) 120 | filePath += ".o" 121 | 122 | fn, err := os.Create(filePath) 123 | if err != nil { 124 | return err 125 | } 126 | // Remove if temp 127 | outputFd = fn 128 | outputFile = fn.Name() 129 | } else { 130 | fn, err := os.Create(outputFile) 131 | if err != nil { 132 | return err 133 | } 134 | outputFd = fn 135 | outputFile = fn.Name() 136 | } 137 | 138 | // Create and start a fork of the default spinner. 139 | var buildSpinner *pterm.SpinnerPrinter 140 | if opts.Local { 141 | buildScript, err := getBuildScript(opts.BuildScript) 142 | if err != nil { 143 | return fmt.Errorf("could not load build script: %v", err) 144 | } 145 | if opts.BuildScriptOutput { 146 | fmt.Printf("%s\n", buildScript) 147 | return nil 148 | } 149 | 150 | buildSpinner, _ = pterm.DefaultSpinner.Start("Compiling BPF program locally") 151 | if err := buildLocal(ctx, opts, buildScript, inputFile, outputFile); err != nil { 152 | buildSpinner.UpdateText("Failed to compile BPF program locally") 153 | buildSpinner.Fail() 154 | return err 155 | } 156 | } else { 157 | buildSpinner, _ = pterm.DefaultSpinner.Start("Compiling BPF program") 158 | if err := buildDocker(ctx, opts, inputFile, outputFile); err != nil { 159 | buildSpinner.UpdateText("Failed to compile BPF program") 160 | buildSpinner.Fail() 161 | return err 162 | } 163 | } 164 | buildSpinner.UpdateText(fmt.Sprintf("Successfully compiled \"%s\" and wrote it to \"%s\"", inputFile, outputFile)) 165 | buildSpinner.Success() // Resolve spinner with success message. 166 | 167 | if opts.BinaryOnly { 168 | return nil 169 | } 170 | 171 | if len(args) == 1 { 172 | return fmt.Errorf("must specify a registry to package the output or run with '--binary-only'") 173 | } 174 | 175 | // TODO: Figure out this hack, file.Seek() didn't seem to work 176 | outputFd.Close() 177 | reopened, err := os.Open(outputFile) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | elfBytes, err := ioutil.ReadAll(reopened) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | registrySpinner, _ := pterm.DefaultSpinner.Start("Packaging BPF program") 188 | 189 | reg, err := content.NewOCI(opts.general.OCIStorageDir) 190 | if err != nil { 191 | registrySpinner.UpdateText("Failed to initialize registry") 192 | registrySpinner.Fail() 193 | return err 194 | } 195 | registryRef := args[1] 196 | ebpfReg := spec.NewEbpfOCICLient() 197 | 198 | pkg := &spec.EbpfPackage{ 199 | ProgramFileBytes: elfBytes, 200 | Platform: getPlatformInfo(ctx), 201 | } 202 | 203 | if err := ebpfReg.Push(ctx, registryRef, reg, pkg); err != nil { 204 | registrySpinner.UpdateText(fmt.Sprintf("Failed to save BPF OCI image: %s", registryRef)) 205 | registrySpinner.Fail() 206 | return err 207 | } 208 | 209 | registrySpinner.UpdateText(fmt.Sprintf("Saved BPF OCI image to %s", registryRef)) 210 | registrySpinner.Success() 211 | 212 | return nil 213 | } 214 | 215 | func getPlatformInfo(ctx context.Context) *ocispec.Platform { 216 | cmd := exec.CommandContext(ctx, "uname", "-srm") 217 | out, err := cmd.CombinedOutput() 218 | if err != nil { 219 | pterm.Warning.Printfln("Unable to derive platform info: %s", out) 220 | return nil 221 | } 222 | splitOut := strings.Split(string(out), " ") 223 | if len(splitOut) != 3 { 224 | pterm.Warning.Printfln("Unable to derive platform info: %s", out) 225 | return nil 226 | } 227 | return &ocispec.Platform{ 228 | OS: strings.TrimSpace(splitOut[0]), 229 | OSVersion: strings.TrimSpace(splitOut[1]), 230 | Architecture: strings.TrimSpace(splitOut[2]), //remove newline 231 | } 232 | } 233 | 234 | func getBuildScript(path string) ([]byte, error) { 235 | if path != "" { 236 | buildScript, err := os.ReadFile(path) 237 | if err != nil { 238 | return nil, fmt.Errorf("could not read build script: %v", err) 239 | } 240 | return buildScript, nil 241 | } 242 | 243 | return builder.GetBuildScript(), nil 244 | } 245 | 246 | func buildDocker( 247 | ctx context.Context, 248 | opts *buildOptions, 249 | inputFile, outputFile string, 250 | ) error { 251 | // TODO: debug log this 252 | wd, err := os.Getwd() 253 | if err != nil { 254 | return err 255 | } 256 | 257 | dockerArgs := []string{ 258 | "run", 259 | "-v", 260 | fmt.Sprintf("%s:/usr/src/bpf", wd), 261 | } 262 | 263 | if len(opts.CFlags) > 0 { 264 | dockerArgs = append(dockerArgs, "--env", fmt.Sprintf("CFLAGS=%s", strings.Join(opts.CFlags, " "))) 265 | } 266 | dockerArgs = append(dockerArgs, opts.BuildImage, inputFile, outputFile) 267 | 268 | dockerCmd := exec.CommandContext(ctx, opts.Builder, dockerArgs...) 269 | byt, err := dockerCmd.CombinedOutput() 270 | if err != nil { 271 | fmt.Printf("%s\n", byt) 272 | return err 273 | } 274 | return nil 275 | } 276 | 277 | func buildLocal( 278 | ctx context.Context, 279 | opts *buildOptions, 280 | buildScript []byte, 281 | inputFile, 282 | outputFile string, 283 | ) error { 284 | // Pass the script into sh via stdin, then arguments 285 | // TODO: need to handle CWD gracefully 286 | shCmd := exec.CommandContext(ctx, "sh", "-s", "--", inputFile, outputFile) 287 | shCmd.Env = []string{ 288 | fmt.Sprintf("CFLAGS=%s", strings.Join(opts.CFlags, " ")), 289 | } 290 | stdin, err := shCmd.StdinPipe() 291 | if err != nil { 292 | return err 293 | } 294 | // Write the script to stdin 295 | go func() { 296 | defer stdin.Close() 297 | io.WriteString(stdin, string(buildScript)) 298 | }() 299 | 300 | out, err := shCmd.CombinedOutput() 301 | pterm.Info.Printf("%s\n", out) 302 | if err != nil { 303 | return err 304 | } 305 | return nil 306 | } 307 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/describe/describe.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "github.com/pterm/pterm" 5 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 6 | "github.com/solo-io/bumblebee/pkg/spec" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/pflag" 9 | ) 10 | 11 | type describeOptions struct { 12 | general *options.GeneralOptions 13 | } 14 | 15 | func addToFlags(flags *pflag.FlagSet, opts *describeOptions) {} 16 | 17 | func Command(opts *options.GeneralOptions) *cobra.Command { 18 | describeOptions := &describeOptions{ 19 | general: opts, 20 | } 21 | cmd := &cobra.Command{ 22 | Use: "describe BPF_OCI_IMAGE", 23 | Short: "Describe a BPF program via it's OCI ref", 24 | Args: cobra.ExactArgs(1), // image 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | return describe(cmd, args, describeOptions) 27 | }, 28 | SilenceUsage: true, 29 | } 30 | addToFlags(cmd.PersistentFlags(), describeOptions) 31 | return cmd 32 | } 33 | 34 | func describe(cmd *cobra.Command, args []string, opts *describeOptions) error { 35 | // guaranteed to be length 1 36 | ref := args[0] 37 | client := spec.NewEbpfOCICLient() 38 | prog, err := spec.TryFromLocal( 39 | cmd.Context(), 40 | ref, 41 | opts.general.OCIStorageDir, 42 | client, 43 | opts.general.AuthOptions.ToRegistryOptions(), 44 | ) 45 | if err != nil { 46 | return err 47 | } 48 | var ( 49 | platformPanel, authorsPanel, descriptionPanel string 50 | ) 51 | 52 | if prog.Description != "" { 53 | descriptionPanel = pterm.DefaultBox.Sprint(prog.Description) 54 | } else { 55 | descriptionPanel = pterm.DefaultBox.Sprint("No description found") 56 | } 57 | 58 | if prog.Description != "" { 59 | authorsPanel = pterm.DefaultBox.Sprint(prog.Authors) 60 | } else { 61 | authorsPanel = pterm.DefaultBox.Sprint("No Authors found") 62 | } 63 | 64 | if prog.Platform != nil { 65 | platformPanel = pterm.DefaultBox. 66 | WithTitle("Platform"). 67 | Sprintf("%s %s %s", prog.Platform.OS, prog.Platform.OSVersion, prog.Platform.Architecture) 68 | } else { 69 | platformPanel = pterm.DefaultBox.WithTitle("Platform").Sprint("unknown") 70 | } 71 | 72 | panels, _ := pterm.DefaultPanel.WithPanels(pterm.Panels{ 73 | {{Data: descriptionPanel}}, 74 | {{Data: authorsPanel}}, 75 | {{Data: platformPanel}}, 76 | }).Srender() 77 | 78 | pterm.DefaultBox.WithTitle(ref).Println(panels) 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/initialize/init.go: -------------------------------------------------------------------------------- 1 | package initialize // Can't name init because it's a hardcoded const in golang 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "text/template" 9 | 10 | "github.com/manifoldco/promptui" 11 | "github.com/pterm/pterm" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/pflag" 14 | ) 15 | 16 | type InitOptions struct { 17 | Language string 18 | MapType string 19 | FilePath string 20 | OutputType string 21 | ProgramType string 22 | } 23 | 24 | func addToFlags(flags *pflag.FlagSet, opts *InitOptions) { 25 | flags.StringVarP(&opts.Language, "language", "l", "", "Language to use for the bpf program") 26 | flags.StringVarP(&opts.MapType, "map", "m", "", "Map type to initialize") 27 | flags.StringVarP(&opts.FilePath, "file", "f", "", "File to create skeleton in") 28 | flags.StringVarP(&opts.OutputType, "output-type", "o", "", "The output type for your map") 29 | flags.StringVar(&opts.ProgramType, "program-type", "", "The type of program to create (e.g. network, file-system)") 30 | } 31 | 32 | func Command() *cobra.Command { 33 | opts := &InitOptions{} 34 | 35 | cmd := &cobra.Command{ 36 | Use: "init", 37 | Short: "Initialize a sample BPF program", 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | return initialize(opts) 40 | }, 41 | SilenceUsage: true, 42 | } 43 | addToFlags(cmd.PersistentFlags(), opts) 44 | 45 | return cmd 46 | } 47 | 48 | func initialize(opts *InitOptions) error { 49 | var err error 50 | var languageType string 51 | if opts.Language == "" { 52 | languageType, err = selectLanguage() 53 | if err != nil { 54 | return err 55 | } 56 | } else { 57 | err = validateLanguage(opts.Language) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | programType := opts.ProgramType 64 | if programType == "" { 65 | programType, err = selectProgramType() 66 | if err != nil { 67 | return err 68 | } 69 | } 70 | var mapTemplate *templateData 71 | if programType == network { 72 | mapTemplate, err = handleNetworkProgram(opts) 73 | if err != nil { 74 | return err 75 | } 76 | } else if programType == fileSystem { 77 | if opts.MapType != "" || opts.OutputType != "" { 78 | return errors.New("initializing a file system type program with custom map types or output type is not currently supported. Please remove overrides for map type and output type to continue") 79 | } 80 | mapTemplate = &templateData{ 81 | StructData: openAtStruct, 82 | RenderedBody: openAtBody, 83 | RenderedMap: openAtMap, 84 | } 85 | } else { 86 | return fmt.Errorf("%s is not a valid program type", programType) 87 | } 88 | 89 | tmpl := template.Must(template.New("c-file-template").Parse(fileTemplate)) 90 | 91 | fileBuf := &bytes.Buffer{} 92 | if err := tmpl.Execute(fileBuf, mapTemplate); err != nil { 93 | return err 94 | } 95 | 96 | fileLocation := opts.FilePath 97 | if fileLocation == "" { 98 | fileLocation, err = getFileLocation() 99 | if fileLocation == "" && languageType == "C" { 100 | fileLocation = "bee.c" 101 | } 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | 107 | fn, err := os.Create(fileLocation) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | _, err = fn.Write(fileBuf.Bytes()) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | pterm.Success.Println("Successfully wrote skeleton BPF program") 118 | return nil 119 | } 120 | 121 | func handleNetworkProgram(opts *InitOptions) (*templateData, error) { 122 | var err error 123 | mapType := opts.MapType 124 | if mapType == "" { 125 | mapType, err = selectMapType() 126 | if err != nil { 127 | return nil, err 128 | } 129 | } else { 130 | err = validateMapType(mapType) 131 | if err != nil { 132 | return nil, err 133 | } 134 | } 135 | 136 | mapTemplate := mapTypeToTemplateData[mapType] 137 | 138 | outputType := opts.OutputType 139 | if outputType == "" { 140 | outputType, err = selectOutputType() 141 | if err != nil { 142 | return nil, err 143 | } 144 | } else { 145 | err = validateOutputType(outputType) 146 | if err != nil { 147 | return nil, err 148 | } 149 | } 150 | 151 | mapTemplate.MapData.OutputType = mapOutputTypeToTemplateData[outputType] 152 | mapTemplate.FunctionBody.OutputType = mapOutputTypeToTemplateData[outputType] 153 | 154 | mapTmpl := template.Must(template.New("map-tmpl").Parse(mapTemplate.MapData.Template)) 155 | mapBuf := &bytes.Buffer{} 156 | if err := mapTmpl.Execute(mapBuf, mapTemplate.MapData); err != nil { 157 | return nil, err 158 | } 159 | mapTemplate.RenderedMap = mapBuf.String() 160 | 161 | bodyTmpl := template.Must(template.New("body-tmpl").Parse(mapTemplate.FunctionBody.Template)) 162 | bodyBuf := &bytes.Buffer{} 163 | if err := bodyTmpl.Execute(bodyBuf, mapTemplate.FunctionBody); err != nil { 164 | return nil, err 165 | } 166 | mapTemplate.RenderedBody = bodyBuf.String() 167 | 168 | return mapTemplate, nil 169 | } 170 | 171 | func selectLanguage() (string, error) { 172 | return selectValue( 173 | "What language do you wish to use for the filter", 174 | "Selected Language:", 175 | supportedLanguages, 176 | ) 177 | } 178 | 179 | func validateLanguage(lang string) error { 180 | if contains(lang, supportedLanguages) { 181 | return nil 182 | } 183 | return fmt.Errorf("%s is not a valid language type", lang) 184 | } 185 | 186 | func selectProgramType() (string, error) { 187 | return selectValue( 188 | "What type of program to initialize", 189 | "Selected Program Type:", 190 | supportedProgramTypes, 191 | ) 192 | } 193 | 194 | func selectMapType() (string, error) { 195 | return selectValue( 196 | "What type of map should we initialize", 197 | "Selected Map Type:", 198 | supportedMapTypes, 199 | ) 200 | } 201 | 202 | func validateMapType(typ string) error { 203 | if contains(typ, supportedMapTypes) { 204 | return nil 205 | } 206 | return fmt.Errorf("%s is not a valid map type", typ) 207 | } 208 | 209 | func selectOutputType() (string, error) { 210 | return selectValue( 211 | "What type of output would you like from your map", 212 | "Selected Output Type:", 213 | supportedOutputTypes, 214 | ) 215 | } 216 | 217 | func validateOutputType(typ string) error { 218 | if contains(typ, supportedOutputTypes) { 219 | return nil 220 | } 221 | return fmt.Errorf("%s is not a valid output type", typ) 222 | } 223 | 224 | func selectValue(question, success string, options interface{}) (string, error) { 225 | // Add our info func to the promptui FuncMap 226 | promptui.FuncMap["info"] = func(data string) string { 227 | return pterm.Info.Sprintf("%s %s", success, data) 228 | } 229 | 230 | templates := &promptui.SelectTemplates{ 231 | Selected: "{{ . | info }}", 232 | } 233 | 234 | prompt := promptui.Select{ 235 | Label: question, 236 | Items: options, 237 | Templates: templates, 238 | } 239 | _, result, err := prompt.Run() 240 | if err != nil { 241 | return "", err 242 | } 243 | return result, nil 244 | } 245 | 246 | func getFileLocation() (string, error) { 247 | templates := &promptui.PromptTemplates{ 248 | Prompt: "{{ . }} ", 249 | Success: "{{ . | info }} ", 250 | } 251 | prompt := promptui.Prompt{ 252 | Label: "BPF Program File Location", 253 | // Validate: validate, 254 | Templates: templates, 255 | } 256 | return prompt.Run() 257 | 258 | } 259 | 260 | func contains(s string, slice []string) bool { 261 | for _, v := range slice { 262 | if s == v { 263 | return true 264 | } 265 | } 266 | return false 267 | } 268 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/initialize/templates.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | type templateData struct { 4 | StructData string 5 | MapData OutputTypeTemplate 6 | FunctionBody OutputTypeTemplate 7 | RenderedMap string 8 | RenderedBody string 9 | } 10 | 11 | type OutputTypeTemplate struct { 12 | OutputType string 13 | Template string 14 | } 15 | 16 | const ( 17 | languageC = "C" 18 | ) 19 | 20 | // map of language name to description 21 | var supportedLanguages = []string{ 22 | languageC, 23 | } 24 | 25 | const ( 26 | ringBuf = "RingBuffer" 27 | hashMap = "HashMap" 28 | 29 | network = "Network" 30 | fileSystem = "FileSystem" 31 | ) 32 | 33 | var supportedProgramTypes = []string{network, fileSystem} 34 | 35 | var supportedMapTypes = []string{ringBuf, hashMap} 36 | var mapTypeToTemplateData = map[string]*templateData{ 37 | ringBuf: ringbufTemplate(), 38 | hashMap: hashMapTemplate(), 39 | } 40 | 41 | func ringbufTemplate() *templateData { 42 | return &templateData{ 43 | StructData: ringbufStruct, 44 | MapData: OutputTypeTemplate{ 45 | Template: ringbufMapTmpl, 46 | }, 47 | FunctionBody: OutputTypeTemplate{ 48 | Template: ringbufBody, 49 | }, 50 | } 51 | } 52 | 53 | func hashMapTemplate() *templateData { 54 | return &templateData{ 55 | StructData: hashKeyStruct, 56 | MapData: OutputTypeTemplate{ 57 | Template: hashMapTmpl, 58 | }, 59 | FunctionBody: OutputTypeTemplate{ 60 | Template: hashMapBody, 61 | }, 62 | } 63 | } 64 | 65 | const ( 66 | outputPrint = "print" 67 | outputCounter = "counter" 68 | outputGauge = "gauge" 69 | ) 70 | 71 | var supportedOutputTypes = []string{outputPrint, outputCounter, outputGauge} 72 | var mapOutputTypeToTemplateData = map[string]string{ 73 | outputPrint: "print_", 74 | outputCounter: "counter_", 75 | outputGauge: "gauge_", 76 | } 77 | 78 | const openAtStruct = `// This struct represents the data we will gather from the tracepoint to send to our ring buffer map 79 | // The 'bee' runner will watch for entries to our ring buffer and print them out for us 80 | struct event { 81 | // In this example, we have a single field, the filename being opened 82 | char fname[255]; 83 | };` 84 | 85 | const openAtMap = `struct { 86 | __uint(type, BPF_MAP_TYPE_RINGBUF); 87 | __uint(max_entries, 1 << 24); 88 | // Define the type of struct that will be submitted to the ringbuf 89 | // This allows the bee runner to dynamically read and output the data from the ringbuf 90 | __type(value, struct event); 91 | } print_events SEC(".maps");` 92 | 93 | const openAtBody = `// Attach our bpf program to the tracepoint for the openat() syscall 94 | SEC("tracepoint/syscalls/sys_enter_openat") 95 | int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx) 96 | { 97 | // initialize the event struct which we will send the the ring buffer 98 | struct event event = {}; 99 | 100 | // use a bpf helper function to read a string containing the filename 101 | // the filename comes from the tracepoint we are attaching to 102 | bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ctx->args[1]); 103 | 104 | // create a pointer which will be used to access memory in the ring buffer 105 | struct event *ring_val; 106 | 107 | // use another bpf helper to reserve memory for our event in the ring buffer 108 | // our pointer will now point to the correct location we should write our event to 109 | ring_val = bpf_ringbuf_reserve(&print_events, sizeof(struct event), 0); 110 | if (!ring_val) { 111 | return 0; 112 | } 113 | 114 | // copy our event into the ring buffer 115 | memcpy(ring_val, &event, sizeof(struct event)); 116 | 117 | // submit the event to the ring buffer 118 | bpf_ringbuf_submit(ring_val, 0); 119 | 120 | return 0; 121 | }` 122 | 123 | const ringbufMapTmpl = `struct { 124 | __uint(max_entries, 1 << 24); 125 | __uint(type, BPF_MAP_TYPE_RINGBUF); 126 | __type(value, struct event_t); 127 | } {{.OutputType}}events SEC(".maps");` 128 | 129 | const ringbufStruct = `struct event_t { 130 | // 2. Add ringbuf struct data here. 131 | } __attribute__((packed));` 132 | 133 | const ringbufBody = `SEC("kprobe/tcp_v4_connect") 134 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 135 | { 136 | // Init event pointer 137 | struct event_t *event; 138 | 139 | // Reserve a spot in the ringbuffer for our event 140 | event = bpf_ringbuf_reserve(&{{.OutputType}}events, sizeof(struct event_t), 0); 141 | if (!event) { 142 | return 0; 143 | } 144 | 145 | // 3. set data for our event, 146 | // For example: 147 | // event->pid = bpf_get_current_pid_tgid(); 148 | 149 | bpf_ringbuf_submit(event, 0); 150 | 151 | return 0; 152 | }` 153 | 154 | const hashMapTmpl = `struct { 155 | __uint(max_entries, 1 << 24); 156 | __uint(type, BPF_MAP_TYPE_HASH); 157 | __type(key, struct dimensions_t); 158 | __type(value, u64); 159 | } {{.OutputType}}values SEC(".maps");` 160 | 161 | const hashKeyStruct = `struct dimensions_t { 162 | // 2. Add dimensions to your value. This struct will be used as the key in the hash map of your data. 163 | // These will be treated as labels on your metrics. 164 | // In this example we will have single field which contains the PID of the process 165 | u32 pid; 166 | } __attribute__((packed));` 167 | 168 | const hashMapBody = `SEC("kprobe/tcp_v4_connect") 169 | int BPF_KPROBE(tcp_v4_connect, struct sock *sk) 170 | { 171 | // initialize our struct which will be the key in the hash map 172 | struct dimensions_t key; 173 | // initialize variable used to track PID of process calling tcp_v4_connect 174 | u32 pid; 175 | // define variable used to track the count of function calls, and a pointer to it for plumbing 176 | u64 counter; 177 | u64 *counterp; 178 | 179 | // get the pid for the current process which has entered the tcp_v4_connect function 180 | pid = bpf_get_current_pid_tgid(); 181 | key.pid = pid; 182 | 183 | // check if we have an existing value for this key 184 | counterp = bpf_map_lookup_elem(&{{.OutputType}}values, &key); 185 | if (!counterp) { 186 | // debug log to help see how the program works 187 | bpf_printk("no entry found for pid: %u}", key.pid); 188 | // no entry found, so this is the first occurrence, set value to 1 189 | counter = 1; 190 | } 191 | else { 192 | bpf_printk("found existing value '%llu' for pid: %u", *counterp, key.pid); 193 | // we found an entry, so let's increment the existing value for this PID 194 | counter = *counterp + 1; 195 | } 196 | // update our map with the new value of the counter 197 | bpf_map_update_elem(&values, &key, &counter, 0); 198 | 199 | 200 | return 0; 201 | }` 202 | 203 | const fileTemplate = `#include "vmlinux.h" 204 | #include "bpf/bpf_helpers.h" 205 | #include "bpf/bpf_core_read.h" 206 | #include "bpf/bpf_tracing.h" 207 | #include "solo_types.h" 208 | 209 | // 1. Change the license if necessary 210 | char __license[] SEC("license") = "Dual MIT/GPL"; 211 | 212 | {{ .StructData }} 213 | 214 | // This is the definition for the global map which both our 215 | // bpf program and user space program can access. 216 | // More info and map types can be found here: https://www.man7.org/linux/man-pages/man2/bpf.2.html 217 | {{ .RenderedMap }} 218 | 219 | {{ .RenderedBody }} 220 | ` 221 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/list/list.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pterm/pterm" 7 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 8 | "github.com/spf13/cobra" 9 | "oras.land/oras-go/pkg/content" 10 | ) 11 | 12 | func Command(opts *options.GeneralOptions) *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "list", 15 | Short: "List saved OCI image.", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | return list(cmd.Context(), opts) 18 | }, 19 | } 20 | 21 | return cmd 22 | } 23 | 24 | func list(ctx context.Context, opts *options.GeneralOptions) error { 25 | localRegistry, err := content.NewOCI(opts.OCIStorageDir) 26 | if err != nil { 27 | return err 28 | } 29 | localRefs := localRegistry.ListReferences() 30 | 31 | tableData := pterm.TableData{ 32 | []string{"Name", "OS", "OS Version", "Arch"}, 33 | } 34 | for name, ref := range localRefs { 35 | if ref.Platform != nil { 36 | tableData = append(tableData, []string{ 37 | name, 38 | ref.Platform.OS, 39 | ref.Platform.OSVersion, 40 | ref.Platform.Architecture, 41 | }) 42 | } else { 43 | tableData = append(tableData, []string{name, "unknown", "unknown", "unknown"}) 44 | } 45 | 46 | } 47 | 48 | pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() 49 | 50 | return nil 51 | 52 | } 53 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/login/login.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | 13 | "github.com/docker/docker/pkg/term" 14 | "github.com/pkg/errors" 15 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/pflag" 18 | auth "oras.land/oras-go/pkg/auth/docker" 19 | ) 20 | 21 | // this is adapted from oras binary login: 22 | // https://github.com/oras-project/oras/blob/master/cmd/oras/login.go 23 | 24 | type loginOptions struct { 25 | general *options.GeneralOptions 26 | 27 | hostname string 28 | debug bool 29 | fromStdin bool 30 | } 31 | 32 | var stopper chan os.Signal 33 | 34 | func addToFlags(flags *pflag.FlagSet, opts *loginOptions) { 35 | flags.BoolVarP(&opts.debug, "debug", "d", false, "Create a log file 'debug.log' that provides debug logs of loader and TUI execution") 36 | flags.BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin") 37 | } 38 | 39 | func Command(opts *options.GeneralOptions) *cobra.Command { 40 | loginOptions := &loginOptions{ 41 | general: opts, 42 | } 43 | cmd := &cobra.Command{ 44 | Use: "login -u USERNAME -p PASSWORD SERVER_ADDRESS", 45 | Short: "Log in so you can push images to the remote server.", 46 | Long: `Log in to remote registry 47 | 48 | Example - Login with username and password 49 | bee login -u USERNAME -p PASSWORD SERVER_ADDRESS 50 | 51 | Example - Login with username and password from stdin 52 | echo password | bee login -u USERNAME --password-stdin SERVER_ADDRESS 53 | 54 | `, 55 | Args: cobra.ExactArgs(1), // hostname 56 | RunE: func(cmd *cobra.Command, args []string) error { 57 | loginOptions.hostname = args[0] 58 | return run(cmd, args, loginOptions) 59 | }, 60 | SilenceUsage: true, 61 | } 62 | addToFlags(cmd.PersistentFlags(), loginOptions) 63 | return cmd 64 | } 65 | 66 | func run(cmd *cobra.Command, args []string, opts *loginOptions) error { 67 | // Subscribe to signals for terminating the program. 68 | // This is used until management of signals is passed to the TUI 69 | stopper = make(chan os.Signal, 1) 70 | signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) 71 | go func() { 72 | for sig := range stopper { 73 | if sig == os.Interrupt || sig == syscall.SIGTERM { 74 | fmt.Println("got sigterm or interrupt") 75 | os.Exit(0) 76 | } 77 | } 78 | }() 79 | 80 | if opts.debug { 81 | f, err := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 82 | if err != nil { 83 | log.Fatalf("error opening file: %v", err) 84 | } 85 | defer f.Close() 86 | log.SetOutput(f) 87 | } 88 | 89 | // Prepare auth client 90 | cli, err := auth.NewClient(opts.general.AuthOptions.CredentialsFiles...) 91 | if err != nil { 92 | return err 93 | } 94 | // Prompt credential 95 | if opts.fromStdin { 96 | password, err := ioutil.ReadAll(os.Stdin) 97 | if err != nil { 98 | return err 99 | } 100 | opts.general.AuthOptions.Password = strings.TrimSuffix(string(password), "\n") 101 | opts.general.AuthOptions.Password = strings.TrimSuffix(opts.general.AuthOptions.Password, "\r") 102 | } else if opts.general.AuthOptions.Password == "" { 103 | if opts.general.AuthOptions.Username == "" { 104 | username, err := readLine("Username: ", false) 105 | if err != nil { 106 | return err 107 | } 108 | opts.general.AuthOptions.Username = strings.TrimSpace(username) 109 | } 110 | if opts.general.AuthOptions.Username == "" { 111 | if opts.general.AuthOptions.Password, err = readLine("Token: ", true); err != nil { 112 | return err 113 | } else if opts.general.AuthOptions.Password == "" { 114 | return errors.New("token required") 115 | } 116 | } else { 117 | if opts.general.AuthOptions.Password, err = readLine("Password: ", true); err != nil { 118 | return err 119 | } else if opts.general.AuthOptions.Password == "" { 120 | return errors.New("password required") 121 | } 122 | } 123 | } else { 124 | fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") 125 | } 126 | 127 | // Login 128 | if err := cli.Login(cmd.Context(), opts.hostname, opts.general.AuthOptions.Username, opts.general.AuthOptions.Password, opts.general.AuthOptions.Insecure); err != nil { 129 | return err 130 | } 131 | 132 | fmt.Println("Login Succeeded") 133 | return nil 134 | 135 | } 136 | 137 | func readLine(prompt string, slient bool) (string, error) { 138 | fmt.Print(prompt) 139 | if slient { 140 | fd := os.Stdin.Fd() 141 | state, err := term.SaveState(fd) 142 | if err != nil { 143 | return "", err 144 | } 145 | term.DisableEcho(fd, state) 146 | defer term.RestoreTerminal(fd, state) 147 | } 148 | 149 | reader := bufio.NewReader(os.Stdin) 150 | line, _, err := reader.ReadLine() 151 | if err != nil { 152 | return "", err 153 | } 154 | if slient { 155 | fmt.Println() 156 | } 157 | 158 | return string(line), nil 159 | } 160 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/package/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BEE_IMAGE 2 | 3 | FROM $BEE_IMAGE 4 | 5 | USER root 6 | COPY ./store /root/.bumblebee/store/ 7 | 8 | ARG BPF_IMAGE 9 | ENV BPF_IMAGE=$BPF_IMAGE 10 | CMD ./bee run --no-tty ${BPF_IMAGE} 11 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/package/package.go: -------------------------------------------------------------------------------- 1 | package package_cmd 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/pterm/pterm" 11 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 12 | "github.com/solo-io/bumblebee/pkg/internal/version" 13 | "github.com/solo-io/bumblebee/pkg/spec" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/pflag" 16 | "oras.land/oras-go/pkg/content" 17 | "oras.land/oras-go/pkg/oras" 18 | ) 19 | 20 | //go:embed Dockerfile 21 | var packagedDockerfile []byte 22 | 23 | type buildOptions struct { 24 | BeeImage string 25 | Builder string 26 | 27 | general *options.GeneralOptions 28 | } 29 | 30 | func addToFlags(flags *pflag.FlagSet, opts *buildOptions) { 31 | flags.StringVarP(&opts.Builder, "builder", "b", "docker", "Executable to use for docker build command") 32 | flags.StringVar(&opts.BeeImage, "bee-image", "ghcr.io/solo-io/bumblebee/bee:"+version.Version, "Docker image (including tag) to use a base image for packaged image") 33 | } 34 | 35 | func Command(opts *options.GeneralOptions) *cobra.Command { 36 | buildOpts := &buildOptions{ 37 | general: opts, 38 | } 39 | cmd := &cobra.Command{ 40 | Use: "package REGISTRY_REF DOCKER_IMAGE", 41 | Short: "Package a BPF program OCI image with the `bee` runner in a docker image", 42 | Long: ` 43 | The package command is used to package the desired BPF program along with the 'bee' runner in a Docker image. 44 | This means that the resulting docker image is a single, runnable unit to load and attach your BPF proograms. 45 | You can then ship this image around anywhere you run docker images, e.g. K8s. 46 | 47 | Example workflow: 48 | $ bee build examples/tcpconnect/tcpconnect.c tcpconnect 49 | $ bee package tcpconnect bee-tcpconnect:latest 50 | # deploy 'bee-tcpconnect:latest' to K8s cluster 51 | `, 52 | Args: cobra.ExactArgs(2), 53 | RunE: func(cmd *cobra.Command, args []string) error { 54 | return build(cmd.Context(), args, buildOpts) 55 | }, 56 | SilenceUsage: true, // Usage on error is bad 57 | } 58 | 59 | cmd.OutOrStdout() 60 | 61 | // Init flags 62 | addToFlags(cmd.PersistentFlags(), buildOpts) 63 | 64 | return cmd 65 | } 66 | 67 | func build(ctx context.Context, args []string, opts *buildOptions) error { 68 | 69 | reg, err := content.NewOCI(opts.general.OCIStorageDir) 70 | if err != nil { 71 | return err 72 | } 73 | registryRef := args[0] 74 | 75 | packagingSpinner, _ := pterm.DefaultSpinner.Start("Packaging BPF and bee image") 76 | tmpDir, _ := os.MkdirTemp("", "bee_oci_store") 77 | tmpStore := tmpDir + "/store" 78 | err = os.Mkdir(tmpStore, 0755) 79 | if err != nil { 80 | packagingSpinner.UpdateText(fmt.Sprintf("Failed to create temp dir: %s", tmpStore)) 81 | packagingSpinner.Fail() 82 | return err 83 | } 84 | if opts.general.Verbose { 85 | fmt.Println("Temp dir name:", tmpDir) 86 | fmt.Println("Temp store:", tmpStore) 87 | } 88 | defer os.RemoveAll(tmpDir) 89 | 90 | tempReg, err := content.NewOCI(tmpStore) 91 | if err != nil { 92 | packagingSpinner.UpdateText(fmt.Sprintf("Failed to initialize temp OCI registry in: %s", tmpStore)) 93 | packagingSpinner.Fail() 94 | return err 95 | } 96 | _, err = oras.Copy(ctx, reg, registryRef, tempReg, "", 97 | oras.WithAllowedMediaTypes(spec.AllowedMediaTypes()), 98 | oras.WithPullByBFS) 99 | if err != nil { 100 | packagingSpinner.UpdateText(fmt.Sprintf("Failed to copy image from '%s' to '%s'", opts.general.OCIStorageDir, tmpStore)) 101 | packagingSpinner.Fail() 102 | return err 103 | } 104 | 105 | dockerfile := tmpDir + "/Dockerfile" 106 | err = os.WriteFile(dockerfile, packagedDockerfile, 0755) 107 | if err != nil { 108 | packagingSpinner.UpdateText(fmt.Sprintf("Failed to write: %s'", dockerfile)) 109 | packagingSpinner.Fail() 110 | return err 111 | } 112 | 113 | packagedImage := args[1] 114 | err = buildPackagedImage(ctx, opts, registryRef, opts.BeeImage, tmpDir, packagedImage) 115 | if err != nil { 116 | packagingSpinner.UpdateText("Docker build of packaged image failed'") 117 | packagingSpinner.Fail() 118 | return err 119 | } 120 | 121 | packagingSpinner.UpdateText(fmt.Sprintf("Packaged image built and tagged at %s", packagedImage)) 122 | packagingSpinner.Success() 123 | return nil 124 | } 125 | 126 | func buildPackagedImage( 127 | ctx context.Context, 128 | opts *buildOptions, 129 | ociImage, beeImage, tmpDir, packagedImage string, 130 | ) error { 131 | dockerArgs := []string{ 132 | "build", 133 | "--build-arg", 134 | fmt.Sprintf("BPF_IMAGE=%s", ociImage), 135 | "--build-arg", 136 | fmt.Sprintf("BEE_IMAGE=%s", beeImage), 137 | tmpDir, 138 | "-t", 139 | packagedImage, 140 | } 141 | dockerCmd := exec.CommandContext(ctx, opts.Builder, dockerArgs...) 142 | byt, err := dockerCmd.CombinedOutput() 143 | if err != nil { 144 | fmt.Printf("%s\n", byt) 145 | return err 146 | } 147 | if opts.general.Verbose { 148 | fmt.Printf("%s\n", byt) 149 | } 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/pull/pull.go: -------------------------------------------------------------------------------- 1 | package pull 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pterm/pterm" 8 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 9 | "github.com/solo-io/bumblebee/pkg/spec" 10 | "github.com/spf13/cobra" 11 | "oras.land/oras-go/pkg/content" 12 | "oras.land/oras-go/pkg/oras" 13 | ) 14 | 15 | type pullOptions struct { 16 | general *options.GeneralOptions 17 | } 18 | 19 | func Command(opts *options.GeneralOptions) *cobra.Command { 20 | pullOpts := &pullOptions{ 21 | general: opts, 22 | } 23 | cmd := &cobra.Command{ 24 | Use: "pull", 25 | Short: "Pull an OCI image from a registry.", 26 | Args: cobra.ExactArgs(1), // Ref 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | return pull(cmd.Context(), pullOpts.general, args[0]) 29 | }, 30 | } 31 | 32 | return cmd 33 | } 34 | 35 | func pull(ctx context.Context, opts *options.GeneralOptions, ref string) error { 36 | 37 | localRegistry, err := content.NewOCI(opts.OCIStorageDir) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | remoteRegistry, err := content.NewRegistry(opts.AuthOptions.ToRegistryOptions()) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | pullSpinner, _ := pterm.DefaultSpinner.Start(fmt.Sprintf("Pulling image %s from remote registry", ref)) 48 | _, err = oras.Copy( 49 | ctx, 50 | remoteRegistry, 51 | ref, 52 | localRegistry, 53 | "", 54 | oras.WithAllowedMediaTypes(spec.AllowedMediaTypes()), 55 | oras.WithPullByBFS, 56 | ) 57 | if err != nil { 58 | pullSpinner.UpdateText(fmt.Sprintf("Failed to pull image %s", ref)) 59 | pullSpinner.Fail() 60 | return err 61 | } 62 | pullSpinner.Success() 63 | return nil 64 | 65 | } 66 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/push/push.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pterm/pterm" 8 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 9 | "github.com/solo-io/bumblebee/pkg/spec" 10 | "github.com/spf13/cobra" 11 | "oras.land/oras-go/pkg/content" 12 | "oras.land/oras-go/pkg/oras" 13 | ) 14 | 15 | type pushOptions struct { 16 | general *options.GeneralOptions 17 | } 18 | 19 | func Command(opts *options.GeneralOptions) *cobra.Command { 20 | pushOpts := &pushOptions{ 21 | general: opts, 22 | } 23 | cmd := &cobra.Command{ 24 | Use: "push", 25 | Short: "Push an OCI image to a specified destination.", 26 | Args: cobra.ExactArgs(1), // Ref 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | return push(cmd.Context(), pushOpts.general, args[0]) 29 | }, 30 | } 31 | 32 | return cmd 33 | } 34 | 35 | func push(ctx context.Context, opts *options.GeneralOptions, ref string) error { 36 | localRegistry, err := content.NewOCI(opts.OCIStorageDir) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | remoteRegistry, err := content.NewRegistry(opts.AuthOptions.ToRegistryOptions()) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | pushSpinner, _ := pterm.DefaultSpinner.Start(fmt.Sprintf("Pushing image %s to remote registry", ref)) 47 | _, err = oras.Copy( 48 | ctx, 49 | localRegistry, 50 | ref, 51 | remoteRegistry, 52 | "", 53 | oras.WithAllowedMediaTypes(spec.AllowedMediaTypes()), 54 | oras.WithPullByBFS, 55 | ) 56 | if err != nil { 57 | pushSpinner.UpdateText(fmt.Sprintf("Failed to push image %s", ref)) 58 | pushSpinner.Fail() 59 | return err 60 | } 61 | pushSpinner.Success() 62 | return nil 63 | 64 | } 65 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/cilium/ebpf/rlimit" 15 | "github.com/pkg/errors" 16 | "github.com/pterm/pterm" 17 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 18 | "github.com/solo-io/bumblebee/pkg/decoder" 19 | "github.com/solo-io/bumblebee/pkg/loader" 20 | "github.com/solo-io/bumblebee/pkg/spec" 21 | "github.com/solo-io/bumblebee/pkg/stats" 22 | "github.com/solo-io/bumblebee/pkg/tui" 23 | "github.com/solo-io/go-utils/contextutils" 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/pflag" 26 | "go.uber.org/zap" 27 | ) 28 | 29 | type runOptions struct { 30 | general *options.GeneralOptions 31 | 32 | debug bool 33 | filter []string 34 | histBuckets []string 35 | histValueKey []string 36 | notty bool 37 | pinMaps string 38 | pinProgs string 39 | promPort uint32 40 | } 41 | 42 | const histBucketsDescription string = "Buckets to use for histogram maps. Format is \"map_name,\"" + 43 | "where is a comma separated list of bucket limits. For example: \"events,[1,2,3,4,5]\"" 44 | 45 | const filterDescription string = "Filter to apply to output from maps. Format is \"map_name,key_name,regex\" " + 46 | "You can define a filter per map, if more than one defined, the last defined filter will take precedence" 47 | 48 | var stopper chan os.Signal 49 | 50 | func addToFlags(flags *pflag.FlagSet, opts *runOptions) { 51 | flags.BoolVarP(&opts.debug, "debug", "d", false, "Create a log file 'debug.log' that provides debug logs of loader and TUI execution") 52 | flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, filterDescription) 53 | flags.StringArrayVarP(&opts.histBuckets, "hist-buckets", "b", []string{}, histBucketsDescription) 54 | flags.StringArrayVarP(&opts.histValueKey, "hist-value-key", "k", []string{}, "Key to use for histogram maps. Format is \"map_name,key_name\"") 55 | flags.BoolVar(&opts.notty, "no-tty", false, "Set to true for running without a tty allocated, so no interaction will be expected or rich output will done") 56 | flags.StringVar(&opts.pinMaps, "pin-maps", "", "Directory to pin maps to, left unpinned if empty") 57 | flags.StringVar(&opts.pinProgs, "pin-progs", "", "Directory to pin progs to, left unpinned if empty") 58 | flags.Uint32Var(&opts.promPort, "prom-port", 9091, "Specify the Prometheus listener port") 59 | } 60 | 61 | func Command(opts *options.GeneralOptions) *cobra.Command { 62 | runOptions := &runOptions{ 63 | general: opts, 64 | } 65 | cmd := &cobra.Command{ 66 | Use: "run BPF_PROGRAM", 67 | Short: "Run a BPF program file or OCI image.", 68 | Long: ` 69 | The bee run command takes a compiled BPF program as input, and runs it using 70 | our generic loader. The supported formats are: file, and OCI image 71 | 72 | To run with a file pass it as the first ARG: 73 | $ bee run bpf-program.o 74 | 75 | To run with a OCI image pass it as the first ARG: 76 | $ bee run localhost:5000/oras:ringbuf-demo 77 | 78 | To run with a filter on the output in the TUI, use the --filter (or -f) flag: 79 | $ bee run --filter="events,comm,node" ghcr.io/solo-io/bumblebee/opensnoop:0.0.7 80 | $ bee run -f="events,comm,node" ghcr.io/solo-io/bumblebee/opensnoop:0.0.7 81 | 82 | To run with multiple filters, use the --filter (or -f) flag multiple times: 83 | $ bee run -f="events_hash,daddr,1.1.1.1" -f="events_ring,daddr,1.1.1.1" ghcr.io/solo-io/bumblebee/tcpconnect:0.0.7 84 | 85 | If your program has histogram output, you can supply the buckets using --buckets (or -b) flag: 86 | TODO(albertlockett) add a program w/ histogram buckets as example 87 | $ bee run -b="events,[1,2,3,4,5]" ghcr.io/solo-io/bumblebee/TODO:0.0.7 88 | `, 89 | Args: cobra.ExactArgs(1), // Filename or image 90 | RunE: func(cmd *cobra.Command, args []string) error { 91 | return run(cmd, args, runOptions) 92 | }, 93 | SilenceUsage: true, 94 | } 95 | addToFlags(cmd.PersistentFlags(), runOptions) 96 | return cmd 97 | } 98 | 99 | func run(cmd *cobra.Command, args []string, opts *runOptions) error { 100 | ctx, err := buildContext(cmd.Context(), opts.debug) 101 | if err != nil { 102 | return err 103 | } 104 | contextutils.LoggerFrom(ctx).Info("starting bee run") 105 | if opts.notty { 106 | pterm.DisableStyling() 107 | } 108 | 109 | progLocation := args[0] 110 | progReader, err := getProgram(ctx, opts.general, progLocation) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | // Allow the current process to lock memory for eBPF resources. 116 | if err := rlimit.RemoveMemlock(); err != nil { 117 | return fmt.Errorf("could not raise memory limit (check for sudo or setcap): %v", err) 118 | } 119 | 120 | promProvider, err := stats.NewPrometheusMetricsProvider(ctx, &stats.PrometheusOpts{Port: opts.promPort}) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | progLoader := loader.NewLoader( 126 | decoder.NewDecoderFactory(), 127 | promProvider, 128 | ) 129 | parsedELF, err := progLoader.Parse(ctx, progReader) 130 | if err != nil { 131 | return fmt.Errorf("could not parse BPF program: %w", err) 132 | } 133 | 134 | watchMapOptions, err := parseWatchMapOptions(opts) 135 | if err != nil { 136 | contextutils.LoggerFrom(ctx).Errorf("could not parse watch map options: %v", err) 137 | return err 138 | } 139 | parsedELF.WatchedMapOptions = watchMapOptions 140 | 141 | tuiApp, err := buildTuiApp(&progLoader, progLocation, opts.filter, parsedELF) 142 | if err != nil { 143 | return err 144 | } 145 | loaderOpts := loader.LoadOptions{ 146 | ParsedELF: parsedELF, 147 | Watcher: tuiApp, 148 | PinMaps: opts.pinMaps, 149 | PinProgs: opts.pinProgs, 150 | } 151 | 152 | // bail out before starting TUI if context canceled 153 | if ctx.Err() != nil { 154 | contextutils.LoggerFrom(ctx).Info("before calling tui.Run() context is done") 155 | return ctx.Err() 156 | } 157 | if opts.notty { 158 | fmt.Println("Calling Load...") 159 | loaderOpts.Watcher = loader.NewNoopWatcher() 160 | err = progLoader.Load(ctx, &loaderOpts) 161 | return err 162 | } else { 163 | contextutils.LoggerFrom(ctx).Info("calling tui run()") 164 | err = tuiApp.Run(ctx, progLoader, &loaderOpts) 165 | contextutils.LoggerFrom(ctx).Info("after tui run()") 166 | return err 167 | } 168 | } 169 | 170 | func buildTuiApp(loader *loader.Loader, progLocation string, filterString []string, parsedELF *loader.ParsedELF) (*tui.App, error) { 171 | // TODO: add filter to UI 172 | filter, err := tui.BuildFilter(filterString, parsedELF.WatchedMaps) 173 | if err != nil { 174 | return nil, fmt.Errorf("could not build filter %w", err) 175 | } 176 | appOpts := tui.AppOpts{ 177 | ProgLocation: progLocation, 178 | ParsedELF: parsedELF, 179 | Filter: filter, 180 | } 181 | app := tui.NewApp(&appOpts) 182 | return &app, nil 183 | } 184 | 185 | func getProgram( 186 | ctx context.Context, 187 | opts *options.GeneralOptions, 188 | progLocation string, 189 | ) (io.ReaderAt, error) { 190 | 191 | var ( 192 | progReader io.ReaderAt 193 | programSpinner *pterm.SpinnerPrinter 194 | ) 195 | _, err := os.Stat(progLocation) 196 | if err != nil { 197 | programSpinner, _ = pterm.DefaultSpinner.Start( 198 | fmt.Sprintf("Fetching program from registry: %s", progLocation), 199 | ) 200 | 201 | client := spec.NewEbpfOCICLient() 202 | prog, err := spec.TryFromLocal( 203 | ctx, 204 | progLocation, 205 | opts.OCIStorageDir, 206 | client, 207 | opts.AuthOptions.ToRegistryOptions(), 208 | ) 209 | if err != nil { 210 | programSpinner.UpdateText("Failed to load OCI image") 211 | programSpinner.Fail() 212 | if err, ok := err.(interface { 213 | StackTrace() errors.StackTrace 214 | }); ok { 215 | for _, f := range err.StackTrace() { 216 | fmt.Printf("%+s:%d\n", f, f) 217 | } 218 | } 219 | 220 | return nil, err 221 | } 222 | progReader = bytes.NewReader(prog.ProgramFileBytes) 223 | } else { 224 | programSpinner, _ = pterm.DefaultSpinner.Start( 225 | fmt.Sprintf("Fetching program from file: %s", progLocation), 226 | ) 227 | // Attempt to use file 228 | progReader, err = os.Open(progLocation) 229 | if err != nil { 230 | programSpinner.UpdateText("Failed to open BPF file") 231 | programSpinner.Fail() 232 | return nil, err 233 | } 234 | } 235 | programSpinner.Success() 236 | 237 | return progReader, nil 238 | } 239 | 240 | func buildContext(ctx context.Context, debug bool) (context.Context, error) { 241 | ctx, cancel := context.WithCancel(ctx) 242 | stopper = make(chan os.Signal, 1) 243 | signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) 244 | go func() { 245 | <-stopper 246 | fmt.Println("got sigterm or interrupt") 247 | cancel() 248 | }() 249 | 250 | var sugaredLogger *zap.SugaredLogger 251 | if debug { 252 | cfg := zap.NewDevelopmentConfig() 253 | cfg.OutputPaths = []string{"debug.log"} 254 | cfg.ErrorOutputPaths = []string{"debug.log"} 255 | logger, err := cfg.Build() 256 | if err != nil { 257 | return nil, fmt.Errorf("couldn't create zap logger: '%w'", err) 258 | } 259 | sugaredLogger = logger.Sugar() 260 | } else { 261 | sugaredLogger = zap.NewNop().Sugar() 262 | } 263 | ctx = contextutils.WithExistingLogger(ctx, sugaredLogger) 264 | 265 | return ctx, nil 266 | } 267 | 268 | func parseWatchMapOptions(runOpts *runOptions) (map[string]loader.WatchedMapOptions, error) { 269 | watchMapOptions := make(map[string]loader.WatchedMapOptions) 270 | 271 | for _, bucket := range runOpts.histBuckets { 272 | mapName, bucketLimits, err := parseBucket(bucket) 273 | if err != nil { 274 | return nil, err 275 | } 276 | watchMapOptions[mapName] = loader.WatchedMapOptions{ 277 | HistBuckets: bucketLimits, 278 | } 279 | } 280 | 281 | for _, key := range runOpts.histValueKey { 282 | split := strings.Index(key, ",") 283 | if split == -1 { 284 | return nil, fmt.Errorf("could not parse hist-value-key: %s", key) 285 | } 286 | mapName := key[:split] 287 | valueKey := key[split+1:] 288 | if _, ok := watchMapOptions[mapName]; !ok { 289 | watchMapOptions[mapName] = loader.WatchedMapOptions{} 290 | } 291 | w := watchMapOptions[mapName] 292 | w.HistValueKey = valueKey 293 | watchMapOptions[mapName] = w 294 | } 295 | 296 | return watchMapOptions, nil 297 | } 298 | 299 | func parseBucket(bucket string) (string, []float64, error) { 300 | split := strings.Index(bucket, ",") 301 | if split == -1 { 302 | return "", nil, fmt.Errorf("could not parse bucket: %s", bucket) 303 | } 304 | 305 | mapName := bucket[:split] 306 | bucketLimits := bucket[split+1:] 307 | bucketLimits = strings.TrimPrefix(bucketLimits, "[") 308 | bucketLimits = strings.TrimSuffix(bucketLimits, "]") 309 | buckets := []float64{} 310 | 311 | for _, limit := range strings.Split(bucketLimits, ",") { 312 | bval, err := strconv.ParseFloat(limit, 64) 313 | if err != nil { 314 | return "", nil, fmt.Errorf("could not parse bucket: %s from buckets %s", limit, bucket) 315 | } 316 | buckets = append(buckets, bval) 317 | } 318 | 319 | return mapName, buckets, nil 320 | 321 | } 322 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/tag/tag.go: -------------------------------------------------------------------------------- 1 | package tag 2 | 3 | import ( 4 | "context" 5 | 6 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 7 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 8 | "github.com/spf13/cobra" 9 | "oras.land/oras-go/pkg/content" 10 | ) 11 | 12 | type tagOptions struct { 13 | general *options.GeneralOptions 14 | } 15 | 16 | func Command(opts *options.GeneralOptions) *cobra.Command { 17 | tagOpts := &tagOptions{ 18 | general: opts, 19 | } 20 | cmd := &cobra.Command{ 21 | Use: "tag", 22 | Short: "Add an additional name to a local OCI image.", 23 | Args: cobra.ExactArgs(2), // source, target ref 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | return tag(cmd.Context(), tagOpts.general, args[0], args[1]) 26 | }, 27 | } 28 | 29 | return cmd 30 | } 31 | 32 | func tag( 33 | ctx context.Context, 34 | opts *options.GeneralOptions, 35 | sourceRef, targetRef string, 36 | ) error { 37 | 38 | localRegistry, err := content.NewOCI(opts.OCIStorageDir) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | _, desc, err := localRegistry.Resolve(ctx, sourceRef) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if desc.Annotations != nil { 49 | annotations := desc.Annotations 50 | // note: we have to copy the map, so we don't mess with the original descriptor map 51 | desc.Annotations = make(map[string]string) 52 | for k, v := range annotations { 53 | if k != ocispec.AnnotationRefName { 54 | desc.Annotations[k] = v 55 | } 56 | } 57 | } 58 | 59 | localRegistry.AddReference(targetRef, desc) 60 | err = localRegistry.SaveIndex() 61 | return err 62 | } 63 | -------------------------------------------------------------------------------- /pkg/cli/internal/commands/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/solo-io/bumblebee/pkg/cli/internal/options" 7 | "github.com/solo-io/bumblebee/pkg/internal/version" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/pflag" 10 | ) 11 | 12 | type versionOptions struct { 13 | general *options.GeneralOptions 14 | } 15 | 16 | func addToFlags(flags *pflag.FlagSet, opts *versionOptions) { 17 | } 18 | 19 | func Command(opts *options.GeneralOptions) *cobra.Command { 20 | versionOpts := &versionOptions{ 21 | general: opts, 22 | } 23 | cmd := &cobra.Command{ 24 | Use: "version", 25 | Short: "Display bee Version Information.", 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | fmt.Fprintf(cmd.OutOrStdout(), "%s\n", version.Version) 28 | return nil 29 | }, 30 | SilenceUsage: true, 31 | } 32 | addToFlags(cmd.PersistentFlags(), versionOpts) 33 | return cmd 34 | } 35 | -------------------------------------------------------------------------------- /pkg/cli/internal/options/opts.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "github.com/solo-io/bumblebee/pkg/spec" 5 | "github.com/spf13/pflag" 6 | "oras.land/oras-go/pkg/content" 7 | ) 8 | 9 | func NewGeneralOptions(flags *pflag.FlagSet) *GeneralOptions { 10 | opts := &GeneralOptions{} 11 | opts.addToFlags(flags) 12 | opts.AuthOptions.addToFlags(flags) 13 | return opts 14 | } 15 | 16 | type GeneralOptions struct { 17 | Verbose bool 18 | OCIStorageDir string 19 | ConfigDir string 20 | 21 | AuthOptions AuthOptions 22 | } 23 | 24 | func (opts *GeneralOptions) addToFlags(flags *pflag.FlagSet) { 25 | flags.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") 26 | flags.StringVar(&opts.OCIStorageDir, "storage", spec.EbpfImageDir, "Directory to store OCI images locally") 27 | flags.StringVar(&opts.ConfigDir, "config-dir", spec.EbpfConfigDir, "Directory to bumblebee configuration") 28 | } 29 | 30 | type AuthOptions struct { 31 | CredentialsFiles []string 32 | Username string 33 | Password string 34 | Insecure bool 35 | PlainHTTP bool 36 | } 37 | 38 | func (opts *AuthOptions) addToFlags(flags *pflag.FlagSet) { 39 | flags.StringArrayVarP(&opts.CredentialsFiles, "config", "c", nil, "path to auth configs") 40 | flags.StringVarP(&opts.Username, "username", "u", "", "registry username") 41 | flags.StringVarP(&opts.Password, "password", "p", "", "registry password") 42 | flags.BoolVar(&opts.Insecure, "insecure", false, "allow connections to SSL registry without certs") 43 | flags.BoolVar(&opts.PlainHTTP, "plain-http", false, "use plain http and not https") 44 | } 45 | 46 | func (opts *AuthOptions) ToRegistryOptions() content.RegistryOptions { 47 | return content.RegistryOptions{ 48 | Configs: opts.CredentialsFiles, 49 | Username: opts.Username, 50 | Password: opts.Password, 51 | Insecure: opts.Insecure, 52 | PlainHTTP: opts.PlainHTTP, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "time" 11 | 12 | "github.com/cilium/ebpf/btf" 13 | ) 14 | 15 | const ( 16 | ipv4AddrTypeName = "ipv4_addr" 17 | ipv6AddrTypeName = "ipv6_addr" 18 | durationTypeName = "duration" 19 | ) 20 | 21 | type BinaryDecoder interface { 22 | // DecodeBinaryStruct takes in a raw btf type, and translates 23 | // raw binary data into a map[string]interface{} of that format. 24 | // If the incoming type is not a struct, it will return map[""] 25 | DecodeBtfBinary( 26 | ctx context.Context, typ btf.Type, raw []byte, 27 | ) (map[string]interface{}, error) 28 | } 29 | 30 | type DecoderFactory func() BinaryDecoder 31 | 32 | func NewDecoderFactory() DecoderFactory { 33 | return newDecoder 34 | } 35 | 36 | func newDecoder() BinaryDecoder { 37 | return &decoder{} 38 | } 39 | 40 | type decoder struct { 41 | // Offset within the buffer from which we are currently reading 42 | offset uint32 43 | // Raw binary bytes to read from 44 | raw []byte 45 | } 46 | 47 | func (d *decoder) DecodeBtfBinary( 48 | ctx context.Context, typ btf.Type, raw []byte, 49 | ) (map[string]interface{}, error) { 50 | // Reset values when called 51 | d.raw = raw 52 | d.offset = 0 53 | 54 | switch typedBtf := typ.(type) { 55 | case *btf.Struct: 56 | // Parse the ringbuf event entry into an Event structure. 57 | // buf := bytes.NewBuffer(raw) 58 | result := make(map[string]interface{}) 59 | for _, member := range typedBtf.Members { 60 | val, err := d.processSingleType(member.Type) 61 | if err != nil { 62 | return nil, err 63 | } 64 | result[member.Name] = val 65 | } 66 | return result, nil 67 | case *btf.Typedef: 68 | val, err := d.processSingleType(typedBtf) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return map[string]interface{}{"": val}, nil 73 | case *btf.Float: 74 | val, err := d.processSingleType(typedBtf) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return map[string]interface{}{"": val}, nil 79 | case *btf.Int: 80 | val, err := d.processSingleType(typedBtf) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return map[string]interface{}{"": val}, nil 85 | default: 86 | return nil, fmt.Errorf("unsupported type, %s", typedBtf.TypeName()) 87 | } 88 | } 89 | 90 | func (d *decoder) processSingleType(typ btf.Type) (interface{}, error) { 91 | switch typedMember := typ.(type) { 92 | case *btf.Int: 93 | switch typedMember.Encoding { 94 | case btf.Signed: 95 | if typedMember.Name == "char" { 96 | return d.handleChar(typedMember) 97 | } 98 | return d.handleInt(typedMember) 99 | case btf.Bool: 100 | // TODO 101 | return false, nil 102 | case btf.Char: 103 | // TODO 104 | return "", nil 105 | default: 106 | if typedMember.Name == "unsigned char" { 107 | return d.handleChar(typedMember) 108 | } 109 | // Default encoding seems to be unsigned 110 | return d.handleUint(typedMember) 111 | } 112 | case *btf.Typedef: 113 | // Handle special types 114 | underlying, err := getUnderlyingType(typedMember) 115 | if err != nil { 116 | return nil, err 117 | } 118 | processed, err := d.processSingleType(underlying) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | switch typedMember.Name { 124 | case durationTypeName: 125 | return u64ToDuration(processed) 126 | case ipv4AddrTypeName: 127 | return u32ToIp(processed) 128 | case ipv6AddrTypeName: 129 | return u32ToIp(processed) 130 | default: 131 | return processed, nil 132 | } 133 | 134 | case *btf.Float: 135 | return d.handleFloat(typedMember) 136 | case *btf.Array: 137 | return d.handleArray(typedMember) 138 | default: 139 | return nil, fmt.Errorf("attempting to decode unsupported type, found: %s", typ.TypeName()) 140 | } 141 | } 142 | 143 | // currently only supports strings represented as char arrays 144 | func (d *decoder) handleArray( 145 | typedMember *btf.Array, 146 | ) (interface{}, error) { 147 | typInt, ok := typedMember.Type.(*btf.Int) 148 | if !ok { 149 | return nil, errors.New("only arrays of type *btf.Int (e.g. chars) are supported") 150 | } 151 | if typInt.Name != "char" { 152 | return nil, fmt.Errorf("only arrays with chars (i.e. strings) are supported, found '%s'", typInt.Name) 153 | } 154 | if typInt.Size != 1 { 155 | return nil, fmt.Errorf("expected type size of 1 byte, found '%v'", typInt.Size) 156 | } 157 | length := int(typedMember.Nelems) 158 | slice := make([]byte, length) 159 | for i := 0; i < length; i++ { 160 | buf := bytes.NewBuffer(d.raw[d.offset : d.offset+typInt.Size]) 161 | d.offset += typInt.Size 162 | var val byte 163 | if err := binary.Read(buf, Endianess, &val); err != nil { 164 | return nil, err 165 | } 166 | slice[i] = val 167 | } 168 | n := bytes.IndexByte(slice, 0) 169 | str := string(slice[:n]) 170 | return str, nil 171 | } 172 | 173 | func (d *decoder) handleFloat( 174 | typedMember *btf.Float, 175 | ) (interface{}, error) { 176 | buf := bytes.NewBuffer(d.raw[d.offset : d.offset+typedMember.Size]) 177 | d.offset += typedMember.Size 178 | switch typedMember.Size { 179 | case 8: 180 | var val float64 181 | if err := binary.Read(buf, Endianess, &val); err != nil { 182 | return nil, err 183 | } 184 | return val, nil 185 | case 4: 186 | var val float32 187 | if err := binary.Read(buf, Endianess, &val); err != nil { 188 | return nil, err 189 | } 190 | return val, nil 191 | } 192 | return nil, errors.New("this should never happen") 193 | } 194 | 195 | func (d *decoder) handleUint( 196 | typedMember *btf.Int, 197 | ) (interface{}, error) { 198 | // Default encoding seems to be unsigned 199 | buf := bytes.NewBuffer(d.raw[d.offset : d.offset+typedMember.Size]) 200 | d.offset += typedMember.Size 201 | switch typedMember.Size * 8 { 202 | case 64: 203 | var val uint64 204 | if err := binary.Read(buf, Endianess, &val); err != nil { 205 | return nil, err 206 | } 207 | return val, nil 208 | case 32: 209 | var val uint32 210 | if err := binary.Read(buf, Endianess, &val); err != nil { 211 | return nil, err 212 | } 213 | return val, nil 214 | case 16: 215 | var val uint16 216 | if err := binary.Read(buf, Endianess, &val); err != nil { 217 | return nil, err 218 | } 219 | return val, nil 220 | case 8: 221 | var val uint8 222 | if err := binary.Read(buf, Endianess, &val); err != nil { 223 | return nil, err 224 | } 225 | return val, nil 226 | } 227 | return nil, errors.New("this should never happen") 228 | } 229 | 230 | func (d *decoder) handleChar( 231 | typedMember *btf.Int, 232 | ) (interface{}, error) { 233 | buf := bytes.NewBuffer(d.raw[d.offset : d.offset+1]) 234 | d.offset += 1 235 | var val byte 236 | if err := binary.Read(buf, Endianess, &val); err != nil { 237 | return nil, err 238 | } 239 | return string([]byte{val}), nil 240 | } 241 | 242 | func (d *decoder) handleInt( 243 | typedMember *btf.Int, 244 | ) (interface{}, error) { 245 | buf := bytes.NewBuffer(d.raw[d.offset : d.offset+typedMember.Size]) 246 | d.offset += typedMember.Size 247 | switch typedMember.Size * 8 { 248 | case 64: 249 | var val int64 250 | if err := binary.Read(buf, Endianess, &val); err != nil { 251 | return nil, err 252 | } 253 | return val, nil 254 | case 32: 255 | var val int32 256 | if err := binary.Read(buf, Endianess, &val); err != nil { 257 | return nil, err 258 | } 259 | return val, nil 260 | case 16: 261 | var val int16 262 | if err := binary.Read(buf, Endianess, &val); err != nil { 263 | return nil, err 264 | } 265 | return val, nil 266 | case 8: 267 | var val int8 268 | if err := binary.Read(buf, Endianess, &val); err != nil { 269 | return nil, err 270 | } 271 | return val, nil 272 | } 273 | return nil, errors.New("this should never happen") 274 | } 275 | 276 | func getUnderlyingType(tf *btf.Typedef) (btf.Type, error) { 277 | switch typedMember := tf.Type.(type) { 278 | case *btf.Typedef: 279 | return getUnderlyingType(typedMember) 280 | default: 281 | return typedMember, nil 282 | } 283 | } 284 | 285 | // TODO: Process into string at a later time. 286 | func u64ToDuration(val interface{}) (time.Duration, error) { 287 | u64Val, ok := val.(uint64) 288 | if !ok { 289 | return 0, errors.New("this should never happen") 290 | } 291 | // TODO: Check if overflow somehow 292 | return time.Duration(u64Val), nil 293 | } 294 | 295 | func u32ToIp(val interface{}) (net.IP, error) { 296 | u32Val, ok := val.(uint32) 297 | if !ok { 298 | return net.IP{}, errors.New("this should never happen") 299 | } 300 | ip := make(net.IP, 4) 301 | Endianess.PutUint32(ip, u32Val) 302 | return ip, nil 303 | } 304 | -------------------------------------------------------------------------------- /pkg/decoder/loader_be.go: -------------------------------------------------------------------------------- 1 | //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 2 | // +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 3 | 4 | package decoder 5 | 6 | import "encoding/binary" 7 | 8 | var Endianess = binary.BigEndian 9 | -------------------------------------------------------------------------------- /pkg/decoder/loader_le.go: -------------------------------------------------------------------------------- 1 | //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 2 | // +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 3 | 4 | package decoder 5 | 6 | import "encoding/binary" 7 | 8 | var Endianess = binary.LittleEndian 9 | -------------------------------------------------------------------------------- /pkg/internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var DevVersion = "dev" 4 | 5 | // This will be set by the linker on release builds 6 | var Version string 7 | 8 | func init() { 9 | if Version == "" { 10 | Version = DevVersion 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pkg/loader/watcher.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | type KvPair struct { 4 | Key map[string]string 5 | Value string 6 | Hash uint64 7 | } 8 | 9 | type MapEntry struct { 10 | Name string 11 | Entry KvPair 12 | } 13 | 14 | type MapWatcher interface { 15 | NewRingBuf(name string, keys []string) 16 | NewHashMap(name string, keys []string) 17 | SendEntry(entry MapEntry) 18 | Close() 19 | } 20 | 21 | type noopWatcher struct{} 22 | 23 | func (w *noopWatcher) NewRingBuf(name string, keys []string) { 24 | // noop 25 | } 26 | func (w *noopWatcher) NewHashMap(name string, keys []string) { 27 | // noop 28 | } 29 | func (w *noopWatcher) SendEntry(entry MapEntry) { 30 | // noop 31 | } 32 | func (w *noopWatcher) Close() { 33 | // noop 34 | } 35 | 36 | func NewNoopWatcher() *noopWatcher { 37 | return &noopWatcher{} 38 | } 39 | -------------------------------------------------------------------------------- /pkg/spec/array.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solo-io/bumblebee/82747bf94621117ff338d75d745c6834062f5581/pkg/spec/array.o -------------------------------------------------------------------------------- /pkg/spec/consts.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var ( 9 | EbpfConfigDir = home() + "/.bumblebee" 10 | EbpfImageDir = filepath.Join(EbpfConfigDir, "store") 11 | EbpfCredentialsFile = filepath.Join(EbpfConfigDir, "credentials.json") 12 | ) 13 | 14 | func home() string { 15 | dir, _ := os.UserHomeDir() 16 | return dir 17 | } 18 | -------------------------------------------------------------------------------- /pkg/spec/packaging_suite_test.go: -------------------------------------------------------------------------------- 1 | package spec_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestPackaging(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Packaging Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/spec/spec.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/opencontainers/go-digest" 10 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 11 | "oras.land/oras-go/pkg/content" 12 | "oras.land/oras-go/pkg/oras" 13 | "oras.land/oras-go/pkg/target" 14 | ) 15 | 16 | const ( 17 | configMediaType = "application/ebpf.oci.image.config.v1+json" 18 | eBPFMediaType = "application/ebpf.oci.image.program.v1+binary" 19 | 20 | ebpfFileName = "program.o" 21 | configName = "config.json" 22 | ) 23 | 24 | type EbpfPackage struct { 25 | // File content for eBPF compiled ELF file 26 | ProgramFileBytes []byte 27 | // Human readable description of the program 28 | Description string 29 | // Author(s) of the program 30 | Authors string 31 | // Platform this was built on 32 | Platform *ocispec.Platform 33 | // Nested config object 34 | EbpfConfig 35 | } 36 | 37 | type EbpfConfig struct{} 38 | 39 | type EbpfOCICLient interface { 40 | Push(ctx context.Context, ref string, registry target.Target, pkg *EbpfPackage) error 41 | Pull(ctx context.Context, ref string, registry target.Target) (*EbpfPackage, error) 42 | } 43 | 44 | func NewEbpfOCICLient() EbpfOCICLient { 45 | return &ebpfOCIClient{} 46 | } 47 | 48 | type ebpfOCIClient struct{} 49 | 50 | func AllowedMediaTypes() []string { 51 | return []string{eBPFMediaType, configMediaType} 52 | } 53 | 54 | func (e *ebpfOCIClient) Push( 55 | ctx context.Context, 56 | ref string, 57 | registry target.Target, 58 | pkg *EbpfPackage, 59 | ) error { 60 | 61 | memoryStore := content.NewMemory() 62 | 63 | progDesc, err := memoryStore.Add(ebpfFileName, eBPFMediaType, pkg.ProgramFileBytes) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | configByt, err := json.Marshal(pkg.EbpfConfig) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | configDesc, err := buildConfigDescriptor(configByt, nil) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | memoryStore.Set(configDesc, configByt) 79 | 80 | manifestAnnotations := make(map[string]string) 81 | if pkg.Authors != "" { 82 | manifestAnnotations[ocispec.AnnotationAuthors] = pkg.Authors 83 | } 84 | if pkg.Description != "" { 85 | manifestAnnotations[ocispec.AnnotationDescription] = pkg.Description 86 | } 87 | 88 | manifest, manifestDesc, err := content.GenerateManifest( 89 | &configDesc, 90 | manifestAnnotations, 91 | progDesc, 92 | ) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | manifestDesc.Platform = pkg.Platform 98 | 99 | err = memoryStore.StoreManifest(ref, manifestDesc, manifest) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | _, err = oras.Copy( 105 | ctx, 106 | memoryStore, 107 | ref, 108 | registry, 109 | "", 110 | oras.WithAllowedMediaTypes(AllowedMediaTypes()), 111 | oras.WithPullByBFS, 112 | ) 113 | return err 114 | } 115 | 116 | func (e *ebpfOCIClient) Pull( 117 | ctx context.Context, 118 | ref string, 119 | registry target.Target) (*EbpfPackage, error) { 120 | memoryStore := content.NewMemory() 121 | 122 | manifestDesc, err := oras.Copy( 123 | ctx, 124 | registry, 125 | ref, 126 | memoryStore, 127 | "", 128 | oras.WithAllowedMediaTypes(AllowedMediaTypes()), 129 | ) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | _, ebpfBytes, ok := memoryStore.GetByName(ebpfFileName) 135 | if !ok { 136 | return nil, errors.New("could not find ebpf bytes in manifest") 137 | } 138 | 139 | _, configBytes, ok := memoryStore.GetByName(configName) 140 | if !ok { 141 | return nil, errors.New("could not find ebpf bytes in manifest") 142 | } 143 | 144 | var cfg EbpfConfig 145 | if err := json.Unmarshal(configBytes, &cfg); err != nil { 146 | return nil, err 147 | } 148 | 149 | _, manifestBytes, ok := memoryStore.Get(manifestDesc) 150 | if !ok { 151 | return nil, errors.New("could not find manifest") 152 | } 153 | 154 | var manifest ocispec.Manifest 155 | if err := json.Unmarshal(manifestBytes, &manifest); err != nil { 156 | return nil, fmt.Errorf("could not unmarshal manifest bytes: %w", err) 157 | } 158 | 159 | return &EbpfPackage{ 160 | ProgramFileBytes: ebpfBytes, 161 | Description: manifest.Annotations[ocispec.AnnotationDescription], 162 | Authors: manifest.Annotations[ocispec.AnnotationAuthors], 163 | EbpfConfig: cfg, 164 | Platform: manifestDesc.Platform, 165 | }, nil 166 | } 167 | 168 | // GenerateConfig generates a blank config with optional annotations. 169 | func buildConfigDescriptor( 170 | byt []byte, 171 | annotations map[string]string, 172 | ) (ocispec.Descriptor, error) { 173 | dig := digest.FromBytes(byt) 174 | if annotations == nil { 175 | annotations = map[string]string{} 176 | } 177 | annotations[ocispec.AnnotationTitle] = configName 178 | config := ocispec.Descriptor{ 179 | MediaType: configMediaType, 180 | Digest: dig, 181 | Size: int64(len(byt)), 182 | Annotations: annotations, 183 | } 184 | return config, nil 185 | } 186 | -------------------------------------------------------------------------------- /pkg/spec/spec_test.go: -------------------------------------------------------------------------------- 1 | package spec_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 11 | "github.com/solo-io/bumblebee/pkg/spec" 12 | "oras.land/oras-go/pkg/content" 13 | ) 14 | 15 | var ( 16 | tmpDir string 17 | ) 18 | 19 | var _ = BeforeSuite(func() { 20 | tmpdir, err := os.MkdirTemp("", "") 21 | Expect(err).NotTo(HaveOccurred()) 22 | tmpDir = tmpdir 23 | }) 24 | 25 | var _ = AfterSuite(func() { 26 | os.Remove(tmpDir) 27 | }) 28 | 29 | var _ = Describe("hello", func() { 30 | It("can push", func() { 31 | fn, err := os.Open("array.o") 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | byt, err := io.ReadAll(fn) 35 | Expect(err).NotTo(HaveOccurred()) 36 | 37 | pkg := &spec.EbpfPackage{ 38 | ProgramFileBytes: byt, 39 | Description: "some info", 40 | Authors: "me", 41 | EbpfConfig: spec.EbpfConfig{}, 42 | Platform: &v1.Platform{ 43 | Architecture: "hello", 44 | OS: "linux", 45 | Variant: "test", 46 | }, 47 | } 48 | 49 | reg, err := content.NewOCI(tmpDir) 50 | Expect(err).NotTo(HaveOccurred()) 51 | 52 | registry := spec.NewEbpfOCICLient() 53 | 54 | ctx := context.Background() 55 | err = registry.Push(ctx, "localhost:5000/oras:test9", reg, pkg) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | }) 59 | 60 | It("can pull", func() { 61 | fn, err := os.Open("array.o") 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | byt, err := io.ReadAll(fn) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | pkg := &spec.EbpfPackage{ 68 | ProgramFileBytes: byt, 69 | Description: "some info", 70 | Authors: "me", 71 | EbpfConfig: spec.EbpfConfig{}, 72 | Platform: &v1.Platform{ 73 | Architecture: "hello", 74 | OS: "linux", 75 | Variant: "test", 76 | }, 77 | } 78 | 79 | reg, err := content.NewOCI(tmpDir) 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | registry := spec.NewEbpfOCICLient() 83 | 84 | ctx := context.Background() 85 | newPkg, err := registry.Pull(ctx, "localhost:5000/oras:test9", reg) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | Expect(newPkg.Platform).To(Equal(pkg.Platform)) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /pkg/spec/utils.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "context" 5 | 6 | "oras.land/oras-go/pkg/content" 7 | "oras.land/oras-go/pkg/oras" 8 | ) 9 | 10 | func TryFromLocal( 11 | ctx context.Context, 12 | ref, localStorageDir string, 13 | client EbpfOCICLient, 14 | auth content.RegistryOptions, 15 | ) (*EbpfPackage, error) { 16 | 17 | if localStorageDir == "" { 18 | localStorageDir = EbpfImageDir 19 | } 20 | 21 | localRegistry, err := content.NewOCI(localStorageDir) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if _, _, err := localRegistry.Resolve(ctx, ref); err == nil { 26 | // If we find the image locally, return it 27 | if prog, err := client.Pull(ctx, ref, localRegistry); err == nil { 28 | return prog, nil 29 | } 30 | } 31 | 32 | remoteRegistry, err := content.NewRegistry(auth) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | _, err = oras.Copy( 38 | ctx, 39 | remoteRegistry, 40 | ref, 41 | localRegistry, 42 | "", 43 | oras.WithAllowedMediaTypes(AllowedMediaTypes()), 44 | oras.WithPullByBFS, 45 | ) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // program should now be in the local cache after above copy 51 | return client.Pull(ctx, ref, localRegistry) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/mitchellh/hashstructure/v2" 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/promhttp" 12 | 13 | "github.com/solo-io/go-utils/contextutils" 14 | ) 15 | 16 | const ( 17 | ebpfNamespace = "ebpf_solo_io" 18 | ) 19 | 20 | type PrometheusOpts struct { 21 | Port uint32 22 | MetricsPath string 23 | Registry *prometheus.Registry 24 | } 25 | 26 | func (p *PrometheusOpts) initDefaults() { 27 | if p.Port == 0 { 28 | p.Port = 9091 29 | } 30 | if p.MetricsPath == "" { 31 | p.MetricsPath = "/metrics" 32 | } 33 | } 34 | 35 | func NewPrometheusMetricsProvider(ctx context.Context, opts *PrometheusOpts) (MetricsProvider, error) { 36 | opts.initDefaults() 37 | 38 | serveMux := http.NewServeMux() 39 | handler := promhttp.Handler() 40 | if opts.Registry != nil { 41 | handler = promhttp.InstrumentMetricHandler(opts.Registry, promhttp.HandlerFor(opts.Registry, promhttp.HandlerOpts{})) 42 | } 43 | serveMux.Handle(opts.MetricsPath, handler) 44 | server := &http.Server{ 45 | Addr: fmt.Sprintf(":%d", opts.Port), 46 | Handler: serveMux, 47 | } 48 | go func() { 49 | err := server.ListenAndServe() 50 | if err != nil { 51 | contextutils.LoggerFrom(ctx).Errorf("could not listen for Prometheus metrics: %v", err) 52 | } 53 | }() 54 | 55 | go func() { 56 | <-ctx.Done() 57 | server.Close() 58 | }() 59 | 60 | return &metricsProvider{ 61 | registry: opts.Registry, 62 | }, nil 63 | } 64 | 65 | type MetricsProvider interface { 66 | NewSetCounter(name string, labels []string) SetInstrument 67 | NewIncrementCounter(name string, labels []string) IncrementInstrument 68 | NewGauge(name string, labels []string) SetInstrument 69 | NewHistogram(name string, labels []string, buckets []float64) SetInstrument 70 | } 71 | 72 | type IncrementInstrument interface { 73 | Increment(ctx context.Context, labels map[string]string) 74 | } 75 | 76 | type SetInstrument interface { 77 | Set(ctx context.Context, val int64, labels map[string]string) 78 | } 79 | 80 | type metricsProvider struct { 81 | registry *prometheus.Registry 82 | } 83 | 84 | func (m *metricsProvider) NewSetCounter(name string, labels []string) SetInstrument { 85 | counter := prometheus.NewCounterVec(prometheus.CounterOpts{ 86 | Namespace: ebpfNamespace, 87 | Name: name, 88 | }, labels) 89 | 90 | m.register(counter) 91 | return &setCounter{ 92 | counter: counter, 93 | counterMap: map[uint64]int64{}, 94 | } 95 | } 96 | 97 | func (m *metricsProvider) NewIncrementCounter(name string, labels []string) IncrementInstrument { 98 | counter := prometheus.NewCounterVec(prometheus.CounterOpts{ 99 | Namespace: ebpfNamespace, 100 | Name: name, 101 | }, labels) 102 | 103 | m.register(counter) 104 | return &incrementCounter{ 105 | counter: counter, 106 | } 107 | } 108 | 109 | func (m *metricsProvider) NewGauge(name string, labels []string) SetInstrument { 110 | gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 111 | Namespace: ebpfNamespace, 112 | Name: name, 113 | }, labels) 114 | 115 | m.register(gaugeVec) 116 | return &gauge{ 117 | gauge: gaugeVec, 118 | } 119 | } 120 | 121 | func (m *metricsProvider) NewHistogram(name string, labels []string, buckets []float64) SetInstrument { 122 | h := prometheus.NewHistogramVec(prometheus.HistogramOpts{ 123 | Namespace: ebpfNamespace, 124 | Name: name, 125 | Buckets: buckets, 126 | }, labels) 127 | 128 | m.register(h) 129 | return &histogram{ 130 | histogram: h, 131 | } 132 | 133 | } 134 | 135 | func (m *metricsProvider) register(collectors ...prometheus.Collector) { 136 | if m.registry != nil { 137 | m.registry.MustRegister(collectors...) 138 | return 139 | } 140 | prometheus.MustRegister(collectors...) 141 | } 142 | 143 | type setCounter struct { 144 | counter *prometheus.CounterVec 145 | counterMap map[uint64]int64 146 | } 147 | 148 | func (c *setCounter) Set( 149 | ctx context.Context, 150 | intVal int64, 151 | decodedKey map[string]string, 152 | ) { 153 | 154 | keyHash, err := hashstructure.Hash(decodedKey, hashstructure.FormatV2, nil) 155 | if err != nil { 156 | log.Fatal("This should never happen") 157 | } 158 | 159 | oldVal := c.counterMap[keyHash] 160 | diff := intVal - oldVal 161 | if oldVal == intVal { 162 | return 163 | } 164 | c.counterMap[keyHash] = intVal 165 | c.counter.With(prometheus.Labels(decodedKey)).Add(float64(diff)) 166 | } 167 | 168 | type incrementCounter struct { 169 | counter *prometheus.CounterVec 170 | } 171 | 172 | func (i *incrementCounter) Increment( 173 | ctx context.Context, 174 | decodedKey map[string]string, 175 | ) { 176 | i.counter.With(prometheus.Labels(decodedKey)).Inc() 177 | } 178 | 179 | type gauge struct { 180 | gauge *prometheus.GaugeVec 181 | } 182 | 183 | func (g *gauge) Set( 184 | ctx context.Context, 185 | intVal int64, 186 | decodedKey map[string]string, 187 | ) { 188 | g.gauge.With(prometheus.Labels(decodedKey)).Set(float64(intVal)) 189 | } 190 | 191 | type histogram struct { 192 | histogram *prometheus.HistogramVec 193 | } 194 | 195 | func (h *histogram) Set( 196 | ctx context.Context, 197 | intVal int64, 198 | decodedKey map[string]string, 199 | ) { 200 | h.histogram.With(prometheus.Labels(decodedKey)).Observe(float64(intVal)) 201 | } 202 | -------------------------------------------------------------------------------- /pkg/tui/filter.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/solo-io/bumblebee/pkg/loader" 8 | ) 9 | 10 | func (a *App) filterMatch(entry loader.MapEntry) bool { 11 | if a.filter == nil { 12 | // no filters defined, allow entry 13 | return true 14 | } 15 | filter, ok := a.filter[entry.Name] 16 | if !ok { 17 | // we don't have a filter for this entry's map, allow entry 18 | return true 19 | } 20 | for k, v := range entry.Entry.Key { 21 | if k == filter.KeyField { 22 | if filter.Regex.MatchString(v) { 23 | return true 24 | } 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func BuildFilter(filterString []string, watchedMaps map[string]loader.WatchedMap) (map[string]Filter, error) { 31 | if len(filterString) == 0 { 32 | return nil, nil 33 | } 34 | if (len(filterString) % 3) != 0 { 35 | return nil, fmt.Errorf("filter syntax error, each filter should have 3 fields, found %v fields total", len(filterString)) 36 | } 37 | 38 | filterMap := make(map[string]Filter) 39 | numFilters := len(filterString) / 3 40 | for i := 0; i < numFilters; i++ { 41 | thisFilter := filterString[i*3 : i*3+3] 42 | mapName := thisFilter[0] 43 | labelName := thisFilter[1] 44 | 45 | if _, ok := watchedMaps[mapName]; !ok { 46 | return nil, fmt.Errorf("didnt find map '%v'", mapName) 47 | } 48 | var foundKeyName bool 49 | for _, v := range watchedMaps[mapName].Labels { 50 | if v == labelName { 51 | foundKeyName = true 52 | } 53 | } 54 | if !foundKeyName { 55 | return nil, fmt.Errorf("didnt find key val '%v'", labelName) 56 | } 57 | 58 | regex := regexp.MustCompile(thisFilter[2]) 59 | filterMap[mapName] = Filter{ 60 | MapName: mapName, 61 | KeyField: labelName, 62 | Regex: regex, 63 | } 64 | } 65 | return filterMap, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/tui/tui.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "sort" 8 | "sync" 9 | 10 | "github.com/cilium/ebpf" 11 | "github.com/gdamore/tcell/v2" 12 | "github.com/mitchellh/hashstructure/v2" 13 | "github.com/rivo/tview" 14 | "github.com/solo-io/bumblebee/pkg/loader" 15 | "github.com/solo-io/go-utils/contextutils" 16 | "go.uber.org/zap" 17 | "golang.org/x/sync/errgroup" 18 | ) 19 | 20 | const titleText = `[aqua] __ 21 | /\ \ 22 | \ \ \____ __ __ 23 | \ \ '__'\ /'__'\ /'__'\ // \ 24 | \ \ \L\ \/\ __//\ __/ \\_/ // 25 | \ \_,__/\ \____\ \____\ ''-.._.-''-.._.. -(||)(') 26 | \/___/ \/____/\/____/ ''' 27 | 28 | [aquamarine](powered by solo.io)` 29 | 30 | const helpText = `[chartreuse] [white]Select next table 31 | [chartreuse] [white]Select previous table 32 | [chartreuse] [white]Quit` 33 | 34 | type Filter struct { 35 | MapName string 36 | KeyField string 37 | Regex *regexp.Regexp 38 | } 39 | 40 | type MapValue struct { 41 | Hash uint64 42 | Entries []loader.KvPair 43 | Table *tview.Table 44 | Index int 45 | Type ebpf.MapType 46 | Keys []string 47 | } 48 | 49 | type AppOpts struct { 50 | ProgLocation string 51 | Filter map[string]Filter 52 | ParsedELF *loader.ParsedELF 53 | } 54 | 55 | type App struct { 56 | Entries chan loader.MapEntry 57 | 58 | tviewApp *tview.Application 59 | flex *tview.Flex 60 | progLocation string 61 | filter map[string]Filter 62 | } 63 | 64 | func NewApp(opts *AppOpts) App { 65 | a := App{ 66 | progLocation: opts.ProgLocation, 67 | filter: opts.Filter, 68 | } 69 | return a 70 | } 71 | 72 | var mapOfMaps = make(map[string]MapValue) 73 | var mapMutex = sync.RWMutex{} 74 | var currentIndex int 75 | 76 | func buildTView(logger *zap.SugaredLogger, cancel context.CancelFunc, progLocation string) (*tview.Application, *tview.Flex) { 77 | app := tview.NewApplication() 78 | app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 79 | if event.Key() == tcell.KeyCtrlC || (event.Key() == tcell.KeyRune && event.Rune() == 'q') { 80 | logger.Info("captured ctrl-c in tui, canceling context") 81 | cancel() 82 | } 83 | return event 84 | }) 85 | flex := tview.NewFlex().SetDirection(tview.FlexRow) 86 | flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 87 | if event.Key() == tcell.KeyCtrlN { 88 | nextTable(app) 89 | return nil 90 | } else if event.Key() == tcell.KeyCtrlP { 91 | prevTable(app) 92 | return nil 93 | } 94 | return event 95 | }) 96 | 97 | header := tview.NewGrid().SetRows(0).SetColumns(0, 0) 98 | 99 | title := tview.NewTextView(). 100 | SetTextAlign(tview.AlignCenter).SetDynamicColors(true) 101 | fmt.Fprint(title, titleText) 102 | 103 | fetchText := tview.NewTextView().SetDynamicColors(true) 104 | fmt.Fprintf(fetchText, "Program location: [aqua]%s", progLocation) 105 | 106 | help := tview.NewTextView().SetTextAlign(tview.AlignLeft).SetDynamicColors(true) 107 | fmt.Fprint(help, helpText) 108 | 109 | rightMenu := tview.NewFlex().SetDirection(tview.FlexRow) 110 | fetchMenu := tview.NewFlex().SetDirection(tview.FlexRow). 111 | AddItem(tview.NewBox(), 0, 1, false). 112 | AddItem(fetchText, 0, 1, false). 113 | AddItem(tview.NewBox(), 0, 1, false) 114 | fetchMenu.SetBackgroundColor(tcell.ColorBlack) 115 | rightMenu.AddItem(fetchMenu, 0, 1, false) 116 | rightMenu.AddItem(help, 0, 1, false) 117 | 118 | header.AddItem(title, 0, 0, 1, 1, 0, 0, false) 119 | header.AddItem(rightMenu, 0, 1, 1, 1, 0, 0, false) 120 | 121 | flex.AddItem(header, 10, 0, false) 122 | 123 | return app, flex 124 | } 125 | 126 | func (a *App) Close() { 127 | close(a.Entries) 128 | } 129 | 130 | func (a *App) Run(ctx context.Context, progLoader loader.Loader, loaderOpts *loader.LoadOptions) error { 131 | logger := contextutils.LoggerFrom(ctx) 132 | 133 | ctx, cancel := context.WithCancel(ctx) 134 | app, flex := buildTView(logger, cancel, a.progLocation) 135 | a.tviewApp = app 136 | a.flex = flex 137 | a.Entries = make(chan loader.MapEntry, 20) 138 | 139 | eg := errgroup.Group{} 140 | eg.Go(func() error { 141 | logger.Info("render tui") 142 | err := a.tviewApp.SetRoot(a.flex, true).Run() 143 | logger.Info("tui stopped") 144 | return err 145 | }) 146 | 147 | eg.Go(func() error { 148 | logger.Info("calling watch()") 149 | a.watch(ctx) 150 | logger.Info("returned from watch()") 151 | return nil 152 | }) 153 | 154 | eg.Go(func() error { 155 | logger.Info("calling Load()") 156 | err := progLoader.Load(ctx, loaderOpts) 157 | logger.Info("returned from Load()") 158 | if err != nil { 159 | logger.Error("error loading program") 160 | a.renderLoadError(ctx, err) 161 | go a.tviewApp.QueueUpdateDraw(func() {}) 162 | } 163 | return err 164 | }) 165 | 166 | err := eg.Wait() 167 | logger.Info("after tui waitgroup") 168 | return err 169 | } 170 | 171 | func (a *App) watch(ctx context.Context) { 172 | logger := contextutils.LoggerFrom(ctx) 173 | logger.Info("beginning Watch() loop") 174 | // a.Entries channel will be closed by the Loader 175 | for r := range a.Entries { 176 | if mapOfMaps[r.Name].Type == ebpf.Hash { 177 | a.renderHash(ctx, r) 178 | } else if mapOfMaps[r.Name].Type == ebpf.RingBuf { 179 | a.renderRingBuf(ctx, r) 180 | } 181 | // we need to queue a UI update since tview app is running in a separate goroutine 182 | // don't block here as we still want to process entries as they come in 183 | go a.tviewApp.QueueUpdateDraw(func() {}) 184 | } 185 | logger.Info("no more entries, returning from Watch()") 186 | } 187 | 188 | func (a *App) renderRingBuf(ctx context.Context, incoming loader.MapEntry) { 189 | current := mapOfMaps[incoming.Name] 190 | current.Entries = append(current.Entries, incoming.Entry) 191 | 192 | // update 193 | mapOfMaps[incoming.Name] = current 194 | 195 | // get the instance of Table we will update 196 | table := current.Table 197 | // render the first row containing the keys 198 | c := 0 199 | for i, k := range current.Keys { 200 | cell := tview.NewTableCell(k).SetExpansion(1).SetTextColor(tcell.ColorYellow) 201 | table.SetCell(0, i, cell) 202 | c++ 203 | } 204 | 205 | for r, entry := range current.Entries { 206 | r++ // increment the row index as the 0-th row is taken by the header 207 | ekMap := entry.Key 208 | c := 0 209 | for kk, kv := range current.Keys { 210 | cell := tview.NewTableCell(ekMap[kv]).SetExpansion(1) 211 | table.SetCell(r, kk, cell) 212 | c++ 213 | } 214 | } 215 | } 216 | 217 | func (a *App) renderHash(ctx context.Context, incoming loader.MapEntry) { 218 | logger := contextutils.LoggerFrom(ctx) 219 | current := mapOfMaps[incoming.Name] 220 | if len(current.Entries) == 0 { 221 | newHash, _ := hashstructure.Hash(incoming.Entry.Key, hashstructure.FormatV2, nil) 222 | logger.Infof("empty list, no entries for %v, generated new hash: %v\n", incoming.Entry.Key, newHash) 223 | incoming.Entry.Hash = newHash 224 | current.Entries = append(current.Entries, incoming.Entry) 225 | } else { 226 | incomingHash, _ := hashstructure.Hash(incoming.Entry.Key, hashstructure.FormatV2, nil) 227 | var idx int 228 | var found = false 229 | for idx = range current.Entries { 230 | if current.Entries[idx].Hash == incomingHash { 231 | logger.Infof("found existing entry for %v at index '%v' with hash: %v\n", incoming.Entry.Key, idx, incomingHash) 232 | found = true 233 | break 234 | } 235 | } 236 | if found { 237 | if incoming.Entry.Value == current.Entries[idx].Value { 238 | logger.Infof("for key %v, current value '%v' at index '%v' matches incoming val '%v', continuing...\n", incoming.Entry.Key, current.Entries[idx].Value, idx, incoming.Entry.Value) 239 | return 240 | } 241 | logger.Infof("for existing entry for %v at index '%v' updating val to: %v\n", incoming.Entry.Key, idx, incoming.Entry.Value) 242 | current.Entries[idx].Value = incoming.Entry.Value 243 | } else { 244 | newHash, _ := hashstructure.Hash(incoming.Entry.Key, hashstructure.FormatV2, nil) 245 | incoming.Entry.Hash = newHash 246 | logger.Infof("since no existing entry for %v, appending with hash: %v\n", incoming.Entry.Key, newHash) 247 | current.Entries = append(current.Entries, incoming.Entry) 248 | } 249 | } 250 | 251 | // update 252 | mapOfMaps[incoming.Name] = current 253 | 254 | // get the instance of Table we will update 255 | table := current.Table 256 | // render the first row containing the keys 257 | c := 0 258 | for i, k := range current.Keys { 259 | cell := tview.NewTableCell(k).SetExpansion(1).SetTextColor(tcell.ColorYellow) 260 | table.SetCell(0, i, cell) 261 | c++ 262 | } 263 | // last column in first row is value of the map (i.e. the counter/gauge/etc.) 264 | cell := tview.NewTableCell("value").SetExpansion(1).SetTextColor(tcell.ColorYellow) 265 | table.SetCell(0, c, cell) 266 | 267 | for r, entry := range current.Entries { 268 | r++ // increment the row index as the 0-th row is taken by the header 269 | ekMap := entry.Key 270 | eVal := entry.Value 271 | c := 0 272 | for kk, kv := range current.Keys { 273 | cell := tview.NewTableCell(ekMap[kv]).SetExpansion(1) 274 | table.SetCell(r, kk, cell) 275 | c++ 276 | } 277 | cell := tview.NewTableCell(eVal).SetExpansion(1) 278 | table.SetCell(r, c, cell) 279 | } 280 | } 281 | 282 | func (a *App) renderLoadError(ctx context.Context, err error) { 283 | contextutils.LoggerFrom(ctx).Info("Rendering error") 284 | tv := tview.NewTextView() 285 | tv.SetText(fmt.Sprintf("%s", err)) 286 | tv.SetBorder(true) 287 | tv.SetTitle("Error Loading Program") 288 | tv.SetTitleColor(tcell.ColorRed) 289 | a.flex.AddItem(tv, 0, 1, false) 290 | } 291 | 292 | func (a *App) NewRingBuf(name string, keys []string) { 293 | a.makeMapValue(name, keys, ebpf.RingBuf) 294 | } 295 | 296 | func (a *App) NewHashMap(name string, keys []string) { 297 | a.makeMapValue(name, keys, ebpf.Hash) 298 | } 299 | 300 | func (a *App) SendEntry(entry loader.MapEntry) { 301 | if a.filterMatch(entry) { 302 | a.Entries <- entry 303 | } 304 | } 305 | 306 | func (a *App) makeMapValue(name string, keys []string, mapType ebpf.MapType) { 307 | // get a copy of keys, sort for consistent key/label ordering 308 | keysCopy := make([]string, len(keys)) 309 | copy(keysCopy, keys) 310 | sort.Strings(keysCopy) 311 | 312 | // create the array for containing the entries 313 | entries := make([]loader.KvPair, 0, 10) 314 | 315 | table := tview.NewTable().SetFixed(1, 0) 316 | table.SetBorder(true).SetTitle(name) 317 | 318 | mapMutex.Lock() 319 | i := len(mapOfMaps) 320 | entry := MapValue{ 321 | Table: table, 322 | Index: i, 323 | Type: mapType, 324 | Keys: keysCopy, 325 | Entries: entries, 326 | } 327 | mapOfMaps[name] = entry 328 | mapMutex.Unlock() 329 | 330 | a.tviewApp.QueueUpdateDraw(func() { 331 | a.flex.AddItem(table, 0, 1, false) 332 | if i == 0 { 333 | a.tviewApp.SetFocus(table) 334 | } 335 | }) 336 | } 337 | 338 | func nextTable(app *tview.Application) { 339 | if len(mapOfMaps) <= 1 { 340 | return 341 | } 342 | if currentIndex+1 == len(mapOfMaps) { 343 | currentIndex = 0 344 | } else { 345 | currentIndex++ 346 | } 347 | mapMutex.RLock() 348 | for _, v := range mapOfMaps { 349 | if v.Index == currentIndex { 350 | app.SetFocus(v.Table) 351 | return 352 | } 353 | } 354 | mapMutex.RUnlock() 355 | } 356 | 357 | func prevTable(app *tview.Application) { 358 | if len(mapOfMaps) <= 1 { 359 | return 360 | } 361 | if currentIndex == 0 { 362 | currentIndex = len(mapOfMaps) - 1 363 | } else { 364 | currentIndex-- 365 | } 366 | mapMutex.RLock() 367 | for _, v := range mapOfMaps { 368 | if v.Index == currentIndex { 369 | app.SetFocus(v.Table) 370 | return 371 | } 372 | } 373 | mapMutex.RUnlock() 374 | } 375 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # eBPF Image specifications 2 | 3 | ## Introduction 4 | 5 | The eBPF Image specification defines how to bundle eBPF kernel programs as container images. A compatible eBPF image consists of a eBPF binary file, and Architecture metadata. Currently, the cli in this repo `bee` is the only way to run these images, although the spec is intended to be generic and to provide a standard mechanism to manage the building and running of eBPF modules by any compatible loader. 6 | 7 | ## Terminology: 8 | 9 | | Term | Definition | 10 | |------------------------------------|--------------------------------------------------| 11 | | eBPF Module | The distributable, loadable, and executable unit of code that can run as sandbox programs in Linux Kernel. 12 | | eBPF Image Specification | The specification for storing eBPF modules as container images. 13 | 14 | 15 | 16 | ## eBPF Artifact Image Specification v0.0.0 17 | 18 | - [Description](#description) 19 | - [Layers](#layers) 20 | - [Running OCI Images with bee](#running-oci-images-with-bee) 21 | - [Format](#format) 22 | 23 | ### Description: 24 | 25 | #### Overview: 26 | 27 | Most of the data necessary to running a BTF enabled eBPF program, are contained within the binary iteslf, so fortunately not much other information needs to be stored alongside it. 28 | 29 | #### Layers: 30 | 31 | The content layer always consists of the eBPF module binary. 32 | 33 | The config layer consists of a JSON-formatted string, which currently contains no information, but is available for configuration later should the need arise. 34 | 35 | For the sake of simplicity, the specification only supports a single module per image. 36 | 37 | #### Running OCI Images with bee: 38 | 39 | `bee` takes advantage of a newer linux kernel technology called BTF, so in order to run `eBPF` images, a BTF enabled kernel is required. 40 | 41 | Once this has been verified, these images can be run using `bee run IMAGE_NAME`. 42 | 43 | 44 | ### Format: 45 | 46 | The eBPF OCI Artifact Spec consists of two layers bundled together: 47 | - A layer specifying configuration for the target runtime 48 | - A layer containing the compiled eBPF module itself 49 | 50 | Each layer is associated with its own Media Type, which is stored in the OCI Descriptor for that layer: 51 | 52 | | Media Type | Type | Description | 53 | |------------|------|-------------| 54 | | application/ebpf.oci.image.config.v1+json | JSON Object | Configuration for the Target eBPF module. 55 | | application/ebpf.oci.image.program.v1+binary | binary data (byte array) | Compiled ELF of eBPF module | 56 | 57 | #### Example: 58 | 59 | The following descriptors provide an example of the OCI Image descriptors for an eBPF module stored according to the specification: 60 | ``` 61 | [ 62 | { 63 | "mediaType": "application/ebpf.oci.image.config.v1+json", 64 | "digest": "sha256:d0a165298ae270c5644be8e9938036a3a7a5191f6be03286c40874d761c18abf", 65 | "size": 15, 66 | "annotations": { 67 | "org.opencontainers.image.title": "config.json" 68 | } 69 | }, 70 | { 71 | "mediaType": "application/ebpf.oci.image.program.v1+binary", 72 | "digest": "sha256:5e82b945b59d03620fb360193753cbd08955e30a658dc51735a0fcbc2163d41c", 73 | "size": 1043056, 74 | "annotations": { 75 | "org.opencontainers.image.title": "program.o" 76 | } 77 | } 78 | ] 79 | ``` 80 | 81 | You can use the `bee` tool to take new or existing module code and package it according to the eBPF OCI Spec. 82 | 83 | ## Want more info? 84 | 85 | Refer to the resources below to learn more about: 86 | 87 | - [eBPF](https://ebpf.io/) 88 | - [OCI Artifact](https://github.com/opencontainers/artifacts) 89 | --------------------------------------------------------------------------------