├── .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 |
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 |
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 |
--------------------------------------------------------------------------------