├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build.yml
│ └── testing.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── images
├── feature_view.png
├── help_menu.png
├── map_entry_edit_view.png
├── map_entry_view2.png
└── program_view.png
├── main.go
├── ui
├── errorview.go
├── explorer.go
├── features.go
├── helpview.go
├── map.go
├── map_test.go
└── ui.go
└── utils
├── bpf.go
├── bpf_test.go
├── utils.go
└── utils_test.go
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Let us know of any bugs, crashes, performance, or any other issues you come
4 | across!
5 | title: ''
6 | labels: rc-ebpfmon-bug
7 | assignees: rc-dbogle
8 |
9 | ---
10 |
11 | **Describe the bug**
12 | A clear and concise description of what the bug is.
13 |
14 | **Expected behavior**
15 | A clear and concise description of what you expected to happen.
16 |
17 | **To Reproduce**
18 | Steps to reproduce the behavior:
19 | 1. Loaded bpf programs
20 | 2. Attempted to view certain information
21 | 3. See error
22 |
23 | **Crash report (if applicable)**
24 | * The Go stack trace would be very helpful
25 | * The ebpfmon log file would also be helpful
26 |
27 | **Screenshots**
28 | If applicable, add screenshots to help explain your problem.
29 |
30 | **Platform specifics (please complete the following information):**
31 | - Distro, Go version, Kernel version [e.g Ubuntu 20.04, Go 1.17, 5.13.0-28-generic]
32 | - Architecture [e.g. x86_64, arm64]
33 | - ebpfmon version [e.g. 0.1.0]
34 |
35 | **Additional context**
36 | Add any other context about the problem here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Want something to improve your day-to-day? Let us know!
4 | title: ''
5 | labels: rc-ebpfmon-feature-request
6 | assignees: rc-dbogle
7 | ---
8 |
9 | **At a high level -- can you summarize your request?**
10 |
11 | (E.g.) I would like to be able to combine btf information with the map entries
12 | to have better formatted data
13 |
14 | **Is the current alternative solution?**
15 |
16 | (E.g.) Dump btf information using bpftool yourself
17 |
18 | **Is the feature platform, distro, or architecture dependent?**
19 |
20 | If so please specify which ones
21 |
22 | **Anything else?**
23 |
24 | Please list anything else that might help us implement the feature you're requesting. This could include marked-up screenshots, mockups, etc.
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | go-version: [ '1.18', '1.19', '1.20' ]
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Setup Go ${{ matrix.go-version }}
19 | uses: actions/setup-go@v4
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 |
23 | - name: Display Go version
24 | run: go version
25 |
26 | - name: Get dependencies
27 | run: go get ./...
28 |
29 | - name: Build
30 | run: go build -v ./...
31 |
32 | - name: Run tests
33 | run: go test -v ./...
--------------------------------------------------------------------------------
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: Testing
2 |
3 | on:
4 | pull_request:
5 | types: [opened]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | go-version: [ '1.18', '1.19', '1.20' ]
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Setup Go ${{ matrix.go-version }}
19 | uses: actions/setup-go@v4
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 |
23 | - name: Display Go version
24 | run: go version
25 |
26 | - name: Get dependencies
27 | run: go get ./...
28 |
29 | - name: Build
30 | run: go build -v ./...
31 |
32 | - name: Run tests
33 | run: go test -v ./...
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vmlinux.h
2 | *.swp
3 | ebpfmon
4 | bpftool/**
5 | .output
6 | log.txt
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2023, Red Canary
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: ebpfmon
2 |
3 | ebpfmon:
4 | go build .
5 |
6 | # Just deletes the binary
7 | clean:
8 | rm -rf ebpfmon
9 |
10 | .PHONY: clean ebpfmon
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # ebpfmon
4 | ebpfmon is a tool for monitoring [eBPF](https://ebpf.io/what-is-ebpf/) programs. It is designed to be used with [bpftool](https://github.com/libbpf/bpftool) from the linux kernel. ebpfmon is a TUI (terminal UI) application written in Go that allows you to do real-time monitoring of eBPF programs.
5 |
6 | # Installation
7 | eBPFmon can be built from source or there is a compiled version of it available in the [releases](https://github.com/redcanaryco/ebpfmon/releases)
8 |
9 | ## Required dependencies
10 | - bpftool (installed from a package manager or built from source). This is what ebpfmon uses to get information regarding eBPF programs, maps, etc
11 | - libelf
12 | - zlib
13 |
14 | ## Optional (but highly recommended) dependencies
15 | - libcap-devel
16 | - libbfd
17 |
18 | ## Ubuntu 20.04+
19 | ```bash
20 | $ sudo apt install linux-tools-`uname -r` libelf-dev zlib1g-dev libcap-dev binutils-dev
21 | ```
22 |
23 | ## Amazon Linux 2
24 | ```bash
25 | $ sudo yum install bpftool elfutils-libelf-devel libcap-devel binutils-devel
26 | ```
27 |
28 | ## Rhel, CentOS, Fedora
29 | ```bash
30 | $ sudo dnf install elfutils-libelf-devel libcap-devel zlib-devel binutils-devel bpftool
31 | ```
32 |
33 | ## Debian 11
34 | ```bash
35 | $ sudo apt install bpftool libelf-dev zlib1g-dev libcap-dev binutils-dev
36 | ```
37 |
38 | # Usage
39 | ```bash
40 | $ ./ebpfmon
41 | ```
42 |
43 | NOTE: `bpftool` needs root privileges and so ebpfmon will run `sudo bpftool ...`.
44 | This means you will likely be prompted to enter your sudo password.
45 |
46 |
47 |
48 | # Building from source
49 | ## Additional Dependencies
50 | First and foremost this tool is written in [Go](https://go.dev/learn/) so you will need to have that installed and in your PATH variable. It should work on go 1.18 or later although it's possible it could work on earlier versions. It just hasn't been tested
51 |
52 | Next, make sure to install the following dependencies.
53 | - clang
54 | - llvm
55 |
56 | Simply run the following commands. This will build the `ebpfmon` binary in the current directory
57 | ```bash
58 | $ git clone https://github.com/redcanaryco/ebpfmon.git
59 | $ cd ebpfmon
60 | $ make
61 | ```
62 |
63 | # Documentation
64 | ## Basic navigation
65 | The main view for ebpfmon is the program view. This view shows all the eBPF
66 | programs that are currently loaded on the system. Each pane is selectable and
67 | you can swith between panes by using the `tab` key. In a given pane you can use
68 | the arrow keys to move the scroll the text or list selection up and down.
69 | For lists such as the programs or maps you can press enter to select. Selecting
70 | a program will populate the other other panes with information about that
71 | program. Selecting a map will switch to a map entry view that shows the entries
72 | for that map.
73 |
74 | ## Keybindings
75 | There are a few keybindings that are available in ebpfmon. These are listed
76 | on the help page which can be access by pressing the `F1` key or the `?` key
77 |
78 |
79 |
80 |
81 | ## Program View
82 | To access the program view regardless of which view you are on you can press `Ctrl` and `e`.
83 |
84 |
85 |
86 |
87 | ## Bpf feature view
88 | To access the bpf feature view regardless of which view you are on you can press `Ctrl` and `f`.
89 |
90 |
91 |
92 |
93 | ## Map views
94 | To access the map view simply select a map (if one exists) for the current eBPF program. This will populate the map view with the map entries. You can delete map entries by pressing the `d` key. In the map view you can format the map entry data in various ways. To get to the format section simply press `TAB` while in the map entry list view. You can then use `TAB` to move between the different format options. To get back to the map entry list press `ESC`
95 |
96 |
97 |
98 |
99 |
100 | You can also edit map entries by pressing `ENTER` on a selection. In the edit view you can edit the raw byte values of the map key/value. You can ignore the square brackets
101 |
102 |
103 |
104 |
105 | ## Quitting
106 | To quit the application you can press `q` or `Q`
107 |
108 | ## Going back
109 | Generally the `ESC` key should take you back to the previous view you were on. Also, if you are in the help view or error view, pressing escape should return you to the previous window.
110 |
111 | ## Command Line Arguments
112 | ### `-bpftool`
113 | Allows you to specify the path to the bpftool binary. This is useful if you have
114 | a custom build of bpftool that you want to use. By default it will use the
115 | system's bpftool binary. You can also use an environemnt variable. It will look
116 | in the following order
117 | 1. Check if the `-bpftool` argument was speified on the command line
118 | 2. Check if the environment variable `BPFTOOL_PATH` is set.
119 | 3. Use the system binary
120 |
121 | ### `-logfile`
122 | This argument allows you to specify a file to log to. By default it will log to
123 | `./log.txt`. This is a great file to check when trying to debug issues with the
124 | application as it will log errors that occured during runtime.
125 |
126 | ## Testing
127 | There are some basic tests associated with this project.
128 |
129 | To run the tests simply the following command from the root directory of the project
130 | ```bash
131 | $ go test ./...
132 | ```
133 |
134 | If you want to use your own bpftool binary you can set the `BPFTOOL_PATH` environment variable to the path of your bpftool binary. If not it will default to using the system one.
135 |
136 | ## Important notes about eBPF
137 | ### eBPF maps
138 | - Frozen Maps: If a map is marked as frozen that means no future syscall invocations may alter the map state of map_fd. Write operations from eBPF programs are still possible for a frozen map. This means that bpftool (which is what is uised by ebpfmon) will not be able to alter the map entries. This is a limitation of bpftool and not ebpfmon.
139 | - Ring buffers: bpftool will likely not get any data when trying to query a map of type ringbuf
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module ebpfmon
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/gdamore/tcell/v2 v2.6.0
7 | github.com/rivo/tview v0.0.0-20230406072732-e22ce9588bb4
8 | github.com/sirupsen/logrus v1.9.2
9 | )
10 |
11 | require (
12 | github.com/gdamore/encoding v1.0.0 // indirect
13 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
14 | github.com/mattn/go-runewidth v0.0.14 // indirect
15 | github.com/rivo/uniseg v0.4.3 // indirect
16 | golang.org/x/sys v0.5.0 // indirect
17 | golang.org/x/term v0.5.0 // indirect
18 | golang.org/x/text v0.7.0 // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
5 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
6 | github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
7 | github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
8 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
9 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
10 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
11 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14 | github.com/rivo/tview v0.0.0-20230406072732-e22ce9588bb4 h1:zX+lRcFRPX1jn8A11jxT0dEQhkmUM7pec+9NLK8MiTQ=
15 | github.com/rivo/tview v0.0.0-20230406072732-e22ce9588bb4/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE=
16 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
17 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
18 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
19 | github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
20 | github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
24 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
28 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
29 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
30 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
31 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
32 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
35 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
36 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
40 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
42 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
43 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
44 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
46 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
47 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
48 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
49 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
51 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
52 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
53 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
55 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
56 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
57 |
--------------------------------------------------------------------------------
/images/feature_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redcanaryco/ebpfmon/4eec7c2a3d087f926c3e8e4b25ce0c350833cd5d/images/feature_view.png
--------------------------------------------------------------------------------
/images/help_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redcanaryco/ebpfmon/4eec7c2a3d087f926c3e8e4b25ce0c350833cd5d/images/help_menu.png
--------------------------------------------------------------------------------
/images/map_entry_edit_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redcanaryco/ebpfmon/4eec7c2a3d087f926c3e8e4b25ce0c350833cd5d/images/map_entry_edit_view.png
--------------------------------------------------------------------------------
/images/map_entry_view2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redcanaryco/ebpfmon/4eec7c2a3d087f926c3e8e4b25ce0c350833cd5d/images/map_entry_view2.png
--------------------------------------------------------------------------------
/images/program_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redcanaryco/ebpfmon/4eec7c2a3d087f926c3e8e4b25ce0c350833cd5d/images/program_view.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Description: Main file for the ebpfmon tool
2 | // Author: research@redcanary.com
3 | //
4 | // This tool is used to visualize the bpf programs and maps that are loaded
5 | // on a system. It uses the bpftool binary to get the information about the
6 | // programs and maps and then displays them in a tui using the tview library.
7 | // The user can select a program and then see the maps that are used by that
8 | // program. The user can also see the disassembly of a program by selecting
9 | // the program using the enter key
10 | package main
11 |
12 | import (
13 | "ebpfmon/ui"
14 | "ebpfmon/utils"
15 | "encoding/json"
16 | "flag"
17 | "fmt"
18 | "os"
19 | "os/exec"
20 | "path/filepath"
21 |
22 | log "github.com/sirupsen/logrus"
23 | )
24 |
25 | // The global path to the bpftool binary
26 | var BpftoolPath string
27 |
28 | // A struct for storing the output of `bpftool version -j`
29 | type BpftoolVersionInfo struct {
30 | Version string `json:"version"`
31 | LibbpfVersion string `json:"libbpf_version"`
32 | Features struct {
33 | Libbfd bool `json:"libbfd"`
34 | Llvm bool `json:"llvm"`
35 | Skeletons bool `json:"skeletons"`
36 | Bootstrap bool `json:"bootstrap"`
37 | } `json:"features"`
38 | }
39 |
40 | // A simple struct for storing the config for the app
41 | type Config struct {
42 | // The version of bpftool that is being used
43 | Version BpftoolVersionInfo
44 |
45 | // The path to the bpftool binary
46 | BpftoolPath string
47 |
48 | // Logging verbosity
49 | Verbose bool
50 | }
51 |
52 | func main() {
53 | // Parse the command line arguments
54 | var err error
55 | help := flag.Bool("help", false, "Display help")
56 | verbose := flag.Bool("verbose", false, "Verbose output")
57 | version := flag.Bool("version", false, "Display version information")
58 | logFileArg := flag.String("logfile", "", "Path to log file. Defaults to log.txt")
59 | bpftool_path := flag.String("bpftool", "", "Path to bpftool binary. Defaults to the bpftool located in PATH")
60 |
61 | flag.Parse()
62 |
63 | if *help {
64 | fmt.Println("ebpfmon is a tool for monitoring bpf programs")
65 | flag.Usage()
66 | return
67 | }
68 |
69 | if *version {
70 | fmt.Println("ebpfmon version 0.1")
71 | return
72 | }
73 |
74 | var logFile *os.File
75 | var logpath string
76 | if *logFileArg == "" {
77 | logpath, err = filepath.Abs("./log.txt")
78 | if err != nil {
79 | fmt.Println("Failed to find log file")
80 | os.Exit(1)
81 | }
82 | } else {
83 | logpath, err = filepath.Abs(*logFileArg)
84 | if err != nil {
85 | fmt.Println("Failed to find log file")
86 | os.Exit(1)
87 | }
88 | }
89 | logFile, err = os.OpenFile(logpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
90 | if err != nil {
91 | fmt.Printf("Failed to open log file %s\n%v", logpath, err)
92 | os.Exit(1)
93 | }
94 | defer logFile.Close()
95 | log.SetFormatter(&log.JSONFormatter{})
96 | log.SetOutput(logFile)
97 | if *verbose {
98 | log.SetLevel(log.DebugLevel)
99 | } else {
100 | log.SetLevel(log.InfoLevel)
101 | }
102 |
103 | // Set the global bpftool path variable. It can be set by the command line
104 | // argument or by the BPFTOOL_PATH environment variable. It defaults to
105 | // the bpftool binary in the PATH
106 | bpftoolEnvPath, exists := os.LookupEnv("BPFTOOL_PATH")
107 | if *bpftool_path != "" {
108 | _, err := os.Stat(*bpftool_path)
109 | if err != nil {
110 | fmt.Printf("Failed to find bpftool binary at %s\n", *bpftool_path)
111 | os.Exit(1)
112 | }
113 | BpftoolPath = *bpftool_path
114 | } else if exists {
115 | _, err := os.Stat(bpftoolEnvPath)
116 | if err != nil {
117 | fmt.Printf("Failed to find bpftool binary specified by BPFTOOL_PATH at %s\n", bpftoolEnvPath)
118 | os.Exit(1)
119 | }
120 | BpftoolPath = bpftoolEnvPath
121 | } else {
122 | BpftoolPath, err = exec.LookPath("bpftool")
123 | if err != nil {
124 | fmt.Println("Failed to find compiled version of bpftool")
125 | os.Exit(1)
126 | } else {
127 | BpftoolPath, err = filepath.Abs(BpftoolPath)
128 | if err != nil {
129 | fmt.Println("Failed to find compiled version of bpftool")
130 | os.Exit(1)
131 | }
132 | }
133 | }
134 |
135 | versionInfo := BpftoolVersionInfo{}
136 | stdout, stderr, err := utils.RunCmd(BpftoolPath, "version", "-j")
137 | if err != nil {
138 | fmt.Printf("Failed to run `%s version -j`\n%s\n", BpftoolPath, string(stderr))
139 | os.Exit(1)
140 | }
141 | err = json.Unmarshal(stdout, &versionInfo)
142 | if err != nil {
143 | fmt.Println("Failed to parse bpftool version output")
144 | os.Exit(1)
145 | }
146 |
147 | config := Config{
148 | Version: versionInfo,
149 | BpftoolPath: BpftoolPath,
150 | Verbose: *verbose,
151 | }
152 | utils.BpftoolPath = config.BpftoolPath
153 | app := ui.NewTui(config.BpftoolPath)
154 | log.Info("Starting ebpfmon")
155 |
156 | // Run the app
157 | if err := app.App.Run(); err != nil {
158 | panic(err)
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/ui/errorview.go:
--------------------------------------------------------------------------------
1 | // This page is a simple help modal for showing what the keys are for navigating
2 | package ui
3 |
4 | import "github.com/rivo/tview"
5 |
6 | type ErrorView struct {
7 | modal *tview.Modal
8 | }
9 |
10 | func NewErrorView() *ErrorView {
11 | e := &ErrorView{}
12 | e.buildErrorView()
13 | return e
14 | }
15 |
16 | func (e *ErrorView) buildErrorView() {
17 | modal := tview.NewModal()
18 | modal.SetBorder(true).SetTitle("Error")
19 | modal.SetText("F1: Help\nCtrl-e: Bpf program view\nCtrl-f: Bpf feature view\n'q'|'Q': Quit")
20 | e.modal = modal
21 | }
22 |
23 | func (e *ErrorView) SetError(err string) {
24 | e.modal.SetText(err)
25 | }
26 |
--------------------------------------------------------------------------------
/ui/explorer.go:
--------------------------------------------------------------------------------
1 | // This is the file for managing the main bpf explorer page. This page is used
2 | // to display all the bpf programs that are loaded on the system. It also
3 | // displays the maps that are used by each program. The user can select a
4 | // program and then see the disassembly of that program. The user can also
5 | // see the maps that are used by the program. Those maps can be selected by the
6 | // user which will display the map view/edit page
7 | package ui
8 |
9 | import (
10 | "ebpfmon/utils"
11 | "encoding/json"
12 | "fmt"
13 | "sort"
14 | "strconv"
15 | "strings"
16 | "time"
17 |
18 | "github.com/gdamore/tcell/v2"
19 | "github.com/rivo/tview"
20 | )
21 |
22 | var tui *Tui
23 |
24 | type CgroupProgram struct {
25 | Id int `json:"id"`
26 | AttachType string `json:"attach_type"`
27 | AttachFlags string `json:"attach_flags"`
28 | Name string `json:"name"`
29 | }
30 |
31 | type CgroupInfo struct {
32 | Cgroup string `json:"cgroup"`
33 | Programs []CgroupProgram `json:"programs"`
34 | }
35 |
36 | type XdpInfo struct {
37 | DevName string `json:"devname"`
38 | IfIndex int `json:"ifindex"`
39 | Mode string `json:"mode"`
40 | Id int `json:"id"`
41 | }
42 |
43 | type NetInfo struct {
44 | Xdp []XdpInfo `json:"xdp"`
45 | Tc []TcInfo `json:"tc"`
46 | FlowDissector []FlowDissectorInfo `json:"flow_dissector"`
47 | }
48 |
49 | type PerfInfo struct {
50 | Pid int `json:"pid"`
51 | Fd int `json:"fd"`
52 | ProgId int `json:"prog_id"`
53 | FdType string `json:"fd_type"`
54 | Func string `json:"func",omitempty`
55 | Offset int `json:"offset",omitempty`
56 | Filename string `json:"filename",omitempty`
57 | Tracepoint string `json:"tracepoint",omitempty`
58 | }
59 |
60 | type BpfExplorerView struct {
61 | flex *tview.Flex
62 | programList *tview.List
63 | disassembly *tview.TextView
64 | bpfInfoView *tview.TextView
65 | mapList *tview.List
66 | }
67 |
68 | func applyNetData() {
69 | netInfo := []NetInfo{}
70 | stdout, _, err := utils.RunCmd("sudo", BpftoolPath, "-j", "net", "show")
71 | if err != nil {
72 | tui.DisplayError(fmt.Sprintf("Error running `sudo %s -j net show`: %s\n", BpftoolPath, err))
73 | }
74 | err = json.Unmarshal(stdout, &netInfo)
75 | if err != nil {
76 | tui.DisplayError(fmt.Sprintf("Error decoding json output of `sudo %s -j net show`: %s\n", BpftoolPath, err))
77 | }
78 |
79 | for _, prog := range netInfo {
80 | for _, xdp := range prog.Xdp {
81 | if entry, ok := Programs[xdp.Id]; ok {
82 | entry.Interface = xdp.DevName
83 | Programs[xdp.Id] = entry
84 | }
85 | }
86 | for _, tc := range prog.Tc {
87 | // Update programs with tc data
88 | if entry, ok := Programs[tc.Id]; ok {
89 | entry.Interface = tc.DevName
90 | entry.Name = tc.Name
91 | entry.TcKind = tc.Kind
92 | Programs[tc.Id] = entry
93 | }
94 | }
95 | }
96 | }
97 |
98 | // Try an add extra information to the BpfProgram struct
99 | // If it fails that's ok. It just means we won't have the extra info
100 | // This runs as a go routine
101 | func applyCgroupData() {
102 | cgroupInfo := []CgroupInfo{}
103 | stdout, _, err := utils.RunCmd("sudo", BpftoolPath, "-j", "cgroup", "tree")
104 | if err != nil {
105 | tui.DisplayError(fmt.Sprintf("Error running `sudo %s -j cgroup tree`: %s\n", BpftoolPath, err))
106 | }
107 | err = json.Unmarshal(stdout, &cgroupInfo)
108 | if err != nil {
109 | tui.DisplayError(fmt.Sprintf("Error decoding json output of `sudo %s -j cgroup tree`: %s\n", BpftoolPath, err))
110 | }
111 |
112 | for _, prog := range cgroupInfo {
113 | for _, cgroupProg := range prog.Programs {
114 | if entry, ok := Programs[cgroupProg.Id]; ok {
115 | entry.Cgroup = prog.Cgroup
116 | entry.CgroupAttachFlags = cgroupProg.AttachFlags
117 | entry.CgroupAttachType = cgroupProg.AttachType
118 | Programs[cgroupProg.Id] = entry
119 | }
120 | }
121 | }
122 | }
123 |
124 | // Adds extra context to the BpfProgram struct
125 | // This runs as a go routine
126 | func enrichPrograms() {
127 | applyPerfEventData()
128 | applyCgroupData()
129 | applyNetData()
130 | }
131 |
132 | // Call the bpftool binary using the `perf` option to get the
133 | // list of perf events
134 | // This runs as a go routine
135 | func applyPerfEventData() {
136 | perfInfo := []PerfInfo{}
137 | stdout, _, err := utils.RunCmd("sudo", BpftoolPath, "-j", "perf", "list")
138 | if err != nil {
139 | tui.DisplayError(fmt.Sprintf("Error running `sudo %s -j perf list`: %s\n", BpftoolPath, err))
140 | }
141 |
142 | err = json.Unmarshal(stdout, &perfInfo)
143 | if err != nil {
144 | tui.DisplayError(fmt.Sprintf("Error decoding json output of `sudo %s -j perf list`: %s\n", BpftoolPath, err))
145 | }
146 |
147 | for _, prog := range perfInfo {
148 | if entry, ok := Programs[prog.ProgId]; ok {
149 | entry.Fd = prog.Fd
150 | entry.ProgType = prog.FdType
151 | if prog.FdType == "kprobe" || prog.FdType == "kretprobe" {
152 | entry.AttachPoint = append(entry.AttachPoint, prog.Func)
153 | entry.Offset = prog.Offset
154 | } else if prog.FdType == "uprobe" || prog.FdType == "uretprobe" {
155 | entry.AttachPoint = append(entry.AttachPoint, prog.Filename)
156 | entry.Offset = prog.Offset
157 | } else {
158 | entry.AttachPoint = append(entry.AttachPoint, prog.Tracepoint)
159 | }
160 | Programs[prog.ProgId] = entry
161 | }
162 | }
163 | }
164 |
165 | // Call the bpftool binary to gather the list of available programs
166 | // and return a list of BpfProgram structs
167 | // This runs as a go routine and updates the Programs variable
168 | func updateBpfPrograms() {
169 | // I think the bug is here. We need to intelligently update the Programs variable
170 | // Use a mutex
171 | lock.Lock()
172 |
173 | Programs = map[int]utils.BpfProgram{}
174 | tmp := []utils.BpfProgram{}
175 | stdout, stderr, err := utils.RunCmd("sudo", BpftoolPath, "-j", "prog", "show")
176 | if err != nil {
177 | tui.DisplayError(fmt.Sprintf("Failed to run `sudo %s -j prog show`\n%s\n", BpftoolPath, string(stderr)))
178 | }
179 | err = json.Unmarshal(stdout, &tmp)
180 | if err != nil {
181 | tui.DisplayError(fmt.Sprintf("Failed to run `sudo %s -j prog show`\n%s\n", BpftoolPath, string(stderr)))
182 | }
183 |
184 | for _, program := range tmp {
185 | Programs[program.ProgramId] = program
186 | }
187 |
188 | for _, value := range Programs {
189 | for j, pid := range value.Pids {
190 | cmdline, err := utils.GetProcessCmdline(pid.Pid)
191 | if err == nil {
192 | value.Pids[j].Cmdline = cmdline
193 | }
194 | path, err := utils.GetProcessPath(pid.Pid)
195 | if err == nil {
196 | value.Pids[j].Path = path
197 | }
198 | }
199 | }
200 |
201 | enrichPrograms()
202 | lock.Unlock()
203 | }
204 |
205 | func NewBpfExplorerView(t *Tui) *BpfExplorerView {
206 | // Ensure that this pointer gets set first!
207 | tui = t
208 |
209 | BpfExplorerView := &BpfExplorerView{}
210 | BpfExplorerView.buildProgramList()
211 | BpfExplorerView.buildMapList()
212 | BpfExplorerView.buildDisassemblyView()
213 | BpfExplorerView.buildBpfInfoView()
214 | BpfExplorerView.buildLayout()
215 | return BpfExplorerView
216 | }
217 |
218 | func (b *BpfExplorerView) Update() {
219 | for {
220 | currentSelection := b.programList.GetCurrentItem()
221 |
222 | tui.App.QueueUpdateDraw(func() {
223 | // Remove all the items
224 | b.programList.Clear()
225 | populateList(b.programList)
226 | b.programList.SetCurrentItem(currentSelection)
227 | })
228 | time.Sleep(3 * time.Second)
229 | updateBpfPrograms()
230 | }
231 | }
232 |
233 | func (b *BpfExplorerView) buildLayout() {
234 | // Arrange the UI elements
235 | frame := buildFrame(b.programList)
236 | rightFlex := tview.NewFlex().
237 | SetDirection(tview.FlexRow).
238 | AddItem(b.bpfInfoView, 0, 2, false).
239 | AddItem(b.mapList, 0, 1, false)
240 |
241 | // Alternate layout
242 | aflex := tview.NewFlex().
243 | AddItem(b.disassembly, 0, 2, false).
244 | AddItem(rightFlex, 0, 1, false)
245 |
246 | b.flex = tview.NewFlex()
247 | b.flex.SetDirection(tview.FlexRow)
248 | b.flex.AddItem(frame, 0, 1, true).AddItem(aflex, 0, 2, false)
249 | b.flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
250 | if event.Key() == tcell.KeyTab {
251 | curFocus := tui.App.GetFocus()
252 | if curFocus == b.programList {
253 | tui.App.SetFocus(b.disassembly)
254 | } else if curFocus == b.disassembly {
255 | tui.App.SetFocus(b.bpfInfoView)
256 | } else if curFocus == b.bpfInfoView {
257 | tui.App.SetFocus(b.mapList)
258 | } else if curFocus == b.mapList {
259 | tui.App.SetFocus(b.programList)
260 | }
261 | return nil
262 | } else if event.Key() == tcell.KeyBacktab {
263 | curFocus := tui.App.GetFocus()
264 | if curFocus == b.programList {
265 | tui.App.SetFocus(b.mapList)
266 | } else if curFocus == b.disassembly {
267 | tui.App.SetFocus(b.programList)
268 | } else if curFocus == b.bpfInfoView {
269 | tui.App.SetFocus(b.disassembly)
270 | } else if curFocus == b.mapList {
271 | tui.App.SetFocus(b.bpfInfoView)
272 | }
273 | return nil
274 | }
275 | return event
276 | })
277 | }
278 |
279 | func (b *BpfExplorerView) buildProgramList() {
280 | b.programList = tview.NewList()
281 | b.programList.ShowSecondaryText(false)
282 | populateList(b.programList)
283 |
284 | b.programList.SetSelectedFunc(func(i int, s1, s2 string, r rune) {
285 | b.mapList.Clear()
286 | b.bpfInfoView.Clear()
287 | b.disassembly.Clear()
288 |
289 | lock.Lock()
290 | progId, err := strconv.Atoi(strings.TrimSpace(strings.Split(s1, ":")[0]))
291 | if err != nil {
292 | fmt.Fprintf(b.bpfInfoView, "Failed to parse program id: %s\n", err)
293 | }
294 |
295 | selectedProgram := Programs[progId]
296 |
297 | lock.Unlock()
298 | insns, err := utils.GetBpfProgramDisassembly(progId)
299 | if err != nil {
300 | fmt.Fprintf(b.disassembly, "Error getting disassembly: %s\n", err)
301 | } else {
302 | for _, line := range insns {
303 | fmt.Fprintf(b.disassembly, "%s\n", line)
304 | }
305 | }
306 |
307 | // Get the map info for each map used by the selected program
308 | if len(selectedProgram.MapIds) > 0 {
309 | mapInfo, err := utils.GetBpfMapInfoByIds(selectedProgram.MapIds)
310 | if err != nil {
311 | fmt.Fprintf(b.bpfInfoView, "Failed to get map info: %s\n", err)
312 | }
313 | for _, map_ := range mapInfo {
314 | b.mapList.AddItem(map_.String(), "", 0, nil)
315 | }
316 | }
317 |
318 | // Output the info for the selected program
319 | fmt.Fprintf(b.bpfInfoView, "[blue]Name:[-] %s\n", selectedProgram.Name)
320 | fmt.Fprintf(b.bpfInfoView, "[blue]Tag:[-] %s\n", selectedProgram.Tag)
321 | fmt.Fprintf(b.bpfInfoView, "[blue]ProgramId:[-] %d\n", selectedProgram.ProgramId)
322 | fmt.Fprintf(b.bpfInfoView, "[blue]ProgType:[-] %s\n", selectedProgram.ProgType)
323 | for _, pid := range selectedProgram.Pids {
324 | fmt.Fprintf(b.bpfInfoView, "[blue]Owner:[-] %s\n", pid.Comm)
325 | fmt.Fprintf(b.bpfInfoView, "[blue]OwnerCmdline:[-] %s\n", pid.Cmdline)
326 | fmt.Fprintf(b.bpfInfoView, "[blue]OwnerPath:[-] %s\n", pid.Path)
327 | fmt.Fprintf(b.bpfInfoView, "[blue]OwnerPid:[-] %d\n", pid.Pid)
328 | fmt.Fprintf(b.bpfInfoView, "[blue]OwnerUid:[-] %d\n", pid.Uid)
329 | fmt.Fprintf(b.bpfInfoView, "[blue]OwnerGid:[-] %d\n", pid.Gid)
330 | }
331 | fmt.Fprintf(b.bpfInfoView, "[blue]GplCompat:[-] %v\n", selectedProgram.GplCompatible)
332 | fmt.Fprintf(b.bpfInfoView, "[blue]LoadedAt:[-] %v\n", time.Unix(int64(selectedProgram.LoadedAt), 0))
333 | fmt.Fprintf(b.bpfInfoView, "[blue]BytesXlated:[-] %d\n", selectedProgram.BytesXlated)
334 | fmt.Fprintf(b.bpfInfoView, "[blue]Jited:[-] %v\n", selectedProgram.Jited)
335 | fmt.Fprintf(b.bpfInfoView, "[blue]BytesMemlock:[-] %d\n", selectedProgram.BytesXlated)
336 | fmt.Fprintf(b.bpfInfoView, "[blue]BtfId:[-] %d\n", selectedProgram.BtfId)
337 | if len(selectedProgram.MapIds) > 0 {
338 | fmt.Fprintf(b.bpfInfoView, "[blue]MapIds:[-] %v\n", selectedProgram.MapIds)
339 | }
340 | if len(selectedProgram.Pinned) > 0 {
341 | fmt.Fprintf(b.bpfInfoView, "[blue]Pinned:[-] %s\n", selectedProgram.Pinned)
342 | }
343 | // fmt.Println(selectedProgram.ProgType)
344 | if selectedProgram.ProgType == "kprobe" ||
345 | selectedProgram.ProgType == "kretprobe" ||
346 | selectedProgram.ProgType == "tracepoint" ||
347 | selectedProgram.ProgType == "raw_tracepoint" ||
348 | selectedProgram.ProgType == "uprobe" ||
349 | selectedProgram.ProgType == "uretprobe" {
350 | // fmt.Println(selectedProgram.AttachPoint)
351 | fmt.Fprintf(b.bpfInfoView, "[blue]AttachPoint:[-]\n")
352 | for _, attachPoint := range selectedProgram.AttachPoint {
353 | fmt.Fprintf(b.bpfInfoView, "\t└─%s\n", attachPoint)
354 | }
355 | fmt.Fprintf(b.bpfInfoView, "[blue]Offset:[-] %d\n", selectedProgram.Offset)
356 | fmt.Fprintf(b.bpfInfoView, "[blue]Fd:[-] %d\n", selectedProgram.Fd)
357 | }
358 |
359 | if strings.Contains(selectedProgram.ProgType, "xdp") || strings.Contains(selectedProgram.ProgType, "sched") {
360 | fmt.Fprintf(b.bpfInfoView, "[blue]Interface:[-] %s\n", selectedProgram.Interface)
361 | }
362 | if strings.Contains(selectedProgram.ProgType, "cgroup") {
363 | fmt.Fprintf(b.bpfInfoView, "[blue]Cgroup:[-] %s\n", selectedProgram.Cgroup)
364 | fmt.Fprintf(b.bpfInfoView, "[blue]CgroupAttachType:[-] %s\n", selectedProgram.CgroupAttachType)
365 | fmt.Fprintf(b.bpfInfoView, "[blue]CgroupAttachFlags:[-] %s\n", selectedProgram.CgroupAttachFlags)
366 | }
367 | })
368 | }
369 |
370 | func (b *BpfExplorerView) buildMapList() {
371 | b.mapList = tview.NewList()
372 | b.mapList.ShowSecondaryText(false)
373 | b.mapList.SetBorder(true).SetTitle("Maps")
374 |
375 | b.mapList.SetSelectedFunc(func(i int, s1, s2 string, r rune) {
376 | mapId := strings.TrimSpace(strings.Split(utils.RemoveStringColors(s1), ":")[0])
377 | mapIdInt, _ := strconv.Atoi(mapId)
378 |
379 | mapInfo, err := utils.GetBpfMapInfoByIds([]int{mapIdInt})
380 | if err != nil {
381 | tui.DisplayError(fmt.Sprintf("Failed to get map info: %v\n", err))
382 | } else {
383 | if mapInfo[0].Type == "ringbuf" {
384 | tui.DisplayError("Cannot read from a ringbuf map")
385 | return
386 | }
387 |
388 | err := tui.bpfMapTableView.UpdateMap(mapInfo[0])
389 | if err == nil {
390 | tui.pages.SwitchToPage("maptable")
391 | }
392 | }
393 | })
394 | }
395 |
396 | func buildFrame(programList *tview.List) *tview.Frame {
397 | frame := tview.NewFrame(programList).AddText(" Id: Type Tag Name Attach Point", true, tview.AlignLeft, tcell.ColorWhite)
398 | frame.SetBorder(true).SetTitle("Programs")
399 | return frame
400 | }
401 |
402 | func (b *BpfExplorerView) buildBpfInfoView() {
403 | b.bpfInfoView = tview.NewTextView().
404 | SetDynamicColors(true).
405 | SetRegions(true).
406 | SetWordWrap(true)
407 | b.bpfInfoView.SetBorder(true).SetTitle("Info")
408 | }
409 |
410 | func (b *BpfExplorerView) buildDisassemblyView() {
411 | b.disassembly = tview.NewTextView().
412 | SetDynamicColors(true).
413 | SetRegions(true).
414 | SetWordWrap(true)
415 | b.disassembly.SetBorder(true).SetTitle("Disassembly")
416 | }
417 |
418 | // Populate a tview.List with the output of GetBpfPrograms
419 | func populateList(list *tview.List) {
420 | keys := make([]int, 0, len(Programs))
421 | for k := range Programs {
422 | keys = append(keys, k)
423 | }
424 | sort.Ints(keys)
425 |
426 | for _, k := range keys {
427 | list.AddItem(Programs[k].String(), "", 0, nil)
428 | }
429 | }
430 |
--------------------------------------------------------------------------------
/ui/features.go:
--------------------------------------------------------------------------------
1 | // This file handles the features page of the TUI. It is used to display the
2 | // features that are supported by the kernel and the bpftool binary. It also
3 | // allows the user to search the features to find the ones they are interested
4 | // in
5 | package ui
6 |
7 | import (
8 | "ebpfmon/utils"
9 | "strings"
10 |
11 | "github.com/gdamore/tcell/v2"
12 | "github.com/rivo/tview"
13 | )
14 |
15 | type BpfFeatureView struct {
16 | flex *tview.Flex
17 | }
18 |
19 | func NewBpfFeatureView(tui *Tui) *BpfFeatureView {
20 | result := &BpfFeatureView{}
21 | result.buildFeatureView(tui)
22 | return result
23 | }
24 |
25 | func (b *BpfFeatureView) buildFeatureView(tui *Tui) {
26 | featureView := tview.NewTextView()
27 | featureView.SetBorder(true).SetTitle("Features")
28 | featureView.SetDynamicColors(true)
29 | form := tview.NewForm().
30 | AddInputField("Feature", "", 0, nil, func(text string) {
31 | var filteredText string
32 | var header string
33 | var foundHeader bool
34 | for _, line := range strings.Split(featureInfo, "\n") {
35 | lineLower := strings.ToLower(line)
36 | textLower := strings.ToLower(text)
37 | if strings.HasSuffix(line, ":") || strings.HasSuffix(line, "...") {
38 | foundHeader = true
39 | header = "[blue]" + line + "[-]\n"
40 | } else if strings.Contains(lineLower, textLower) {
41 | if foundHeader {
42 | foundHeader = false
43 | filteredText += header
44 | }
45 | index := strings.Index(lineLower, textLower)
46 | if index != -1 {
47 | filteredText += line[:index] + "[red]" + line[index:index+len(text)] + "[-]" + line[index+len(text):] + "\n"
48 | } else {
49 | filteredText += line + "\n"
50 | }
51 | }
52 | }
53 | featureView.SetText(filteredText)
54 | })
55 | // form.SetHorizontal(true)
56 | form.SetBorder(true).SetTitle("Search")
57 |
58 | flex := tview.NewFlex()
59 | flex.SetDirection(tview.FlexRow)
60 | flex.AddItem(form, 0, 1, false)
61 | flex.AddItem(featureView, 0, 4, false)
62 | flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
63 | if event.Key() == tcell.KeyTab {
64 | if flex.GetItem(0).HasFocus() {
65 | tui.App.SetFocus(flex.GetItem(1))
66 | } else {
67 | tui.App.SetFocus(flex.GetItem(0))
68 | }
69 | return nil
70 | } else if event.Key() == tcell.KeyBacktab {
71 | if flex.GetItem(0).HasFocus() {
72 | tui.App.SetFocus(flex.GetItem(1))
73 | } else {
74 | tui.App.SetFocus(flex.GetItem(0))
75 | }
76 | return nil
77 | }
78 | return event
79 | })
80 |
81 | // Run bpftool feature command and display the output (or stderr on failure)
82 | stdout, stderr, err := utils.RunCmd("sudo", BpftoolPath, "feature", "probe")
83 | if err != nil {
84 | flex.GetItem(1).(*tview.TextView).SetText(string(stderr))
85 | } else {
86 | featureInfo = string(stdout)
87 | flex.GetItem(1).(*tview.TextView).SetText(featureInfo)
88 | }
89 |
90 | b.flex = flex
91 | }
92 |
--------------------------------------------------------------------------------
/ui/helpview.go:
--------------------------------------------------------------------------------
1 | // This page is a simple help modal for showing what the keys are for navigating
2 | package ui
3 |
4 | import "github.com/rivo/tview"
5 |
6 | type HelpView struct {
7 | modal *tview.Modal
8 | }
9 |
10 | func NewHelpView() *HelpView {
11 | v := &HelpView{}
12 | v.buildHelpView()
13 | return v
14 | }
15 |
16 | func (h *HelpView) buildHelpView() {
17 | modal := tview.NewModal()
18 | modal.SetBorder(true).SetTitle("Help")
19 | modal.SetText("F1: Help\nCtrl-e: Bpf program view\nCtrl-f: Bpf feature view\n'q'|'Q': Quit")
20 | h.modal = modal
21 | }
22 |
--------------------------------------------------------------------------------
/ui/map.go:
--------------------------------------------------------------------------------
1 | // This page handles the the view for looking at the map entries of a map. It
2 | // allows the user to select a map and then view the entries in that map. The
3 | // user can also edit the map entries from this view.
4 | package ui
5 |
6 | import (
7 | "ebpfmon/utils"
8 | "encoding/binary"
9 | "fmt"
10 | "strconv"
11 | "strings"
12 |
13 | "github.com/gdamore/tcell/v2"
14 | "github.com/rivo/tview"
15 | log "github.com/sirupsen/logrus"
16 | )
17 |
18 | const (
19 | Hex = 0
20 | Decimal = 1
21 | Char = 2
22 | Raw = 3
23 | )
24 |
25 | const (
26 | DataWidth8 = 1
27 | DataWidth16 = 2
28 | DataWidth32 = 4
29 | DataWidth64 = 8
30 | )
31 | const (
32 | Little = 0
33 | Big = 1
34 | )
35 |
36 | var curFormat = Hex
37 | var curWidth = DataWidth8
38 | var curEndianness = Little
39 |
40 | type BpfMapTableView struct {
41 | pages *tview.Pages
42 | form *tview.Form
43 | filter *tview.Form
44 | table *tview.Table
45 | confirm *tview.Modal
46 | app *Tui
47 | Map utils.BpfMap
48 | MapEntries []utils.BpfMapEntry
49 | }
50 |
51 | func asDecimal(width int, endian int, data []byte) string {
52 | var result string = ""
53 | switch width {
54 | case DataWidth8:
55 | for _, b := range data {
56 | result += strconv.Itoa(int(b)) + " "
57 | }
58 | break
59 | case DataWidth16:
60 | if len(data)%DataWidth16 != 0 {
61 | return ""
62 | }
63 |
64 | if endian == Little {
65 | for i := 0; i < len(data); i += 2 {
66 | result += strconv.Itoa(int(binary.LittleEndian.Uint16(data[i:i+2]))) + " "
67 | }
68 | } else {
69 | for i := 0; i < len(data); i += 2 {
70 | result += strconv.Itoa(int(binary.BigEndian.Uint16(data[i:i+2]))) + " "
71 | }
72 | }
73 | break
74 | case DataWidth32:
75 | if len(data)%DataWidth32 != 0 {
76 | return ""
77 | }
78 | if endian == Little {
79 | for i := 0; i < len(data); i += 4 {
80 | result += strconv.Itoa(int(binary.LittleEndian.Uint32(data[i:i+4]))) + " "
81 | }
82 | } else {
83 | for i := 0; i < len(data); i += 4 {
84 | result += strconv.Itoa(int(binary.BigEndian.Uint32(data[i:i+4]))) + " "
85 | }
86 | }
87 | break
88 | case DataWidth64:
89 | if len(data)%DataWidth64 != 0 {
90 | return ""
91 | }
92 |
93 | if endian == Little {
94 | for i := 0; i < len(data); i += 8 {
95 | result += strconv.Itoa(int(binary.LittleEndian.Uint64(data[i:i+8]))) + " "
96 | }
97 | } else {
98 | for i := 0; i < len(data); i += 8 {
99 | result += strconv.Itoa(int(binary.BigEndian.Uint64(data[i:i+8]))) + " "
100 | }
101 | }
102 | break
103 | }
104 | result = strings.Trim(result, " ")
105 | return result
106 | }
107 |
108 | // Similar to the asDecimal function except it displays the data as hex
109 | func asHex(width int, endian int, data []byte) string {
110 | var result string = ""
111 | switch width {
112 | case DataWidth8:
113 | for _, b := range data {
114 | result += fmt.Sprintf("%#02x", b) + " "
115 | }
116 | break
117 | case DataWidth16:
118 | if len(data)%DataWidth16 != 0 {
119 | return ""
120 | }
121 |
122 | if endian == Little {
123 | for i := 0; i < len(data); i += 2 {
124 | result += fmt.Sprintf("%#04x", binary.LittleEndian.Uint16(data[i:i+2])) + " "
125 | }
126 | } else {
127 | for i := 0; i < len(data); i += 2 {
128 | result += fmt.Sprintf("%#04x", binary.BigEndian.Uint16(data[i:i+2])) + " "
129 | }
130 | }
131 | break
132 | case DataWidth32:
133 | if len(data)%DataWidth32 != 0 {
134 | return ""
135 | }
136 | if endian == Little {
137 | for i := 0; i < len(data); i += 4 {
138 | result += fmt.Sprintf("%#08x", binary.LittleEndian.Uint32(data[i:i+4])) + " "
139 | }
140 | } else {
141 | for i := 0; i < len(data); i += 4 {
142 | result += fmt.Sprintf("%#08x", binary.BigEndian.Uint32(data[i:i+4])) + " "
143 | }
144 | }
145 | break
146 | case DataWidth64:
147 | if len(data)%DataWidth64 != 0 {
148 | return ""
149 | }
150 |
151 | if endian == Little {
152 | for i := 0; i < len(data); i += 8 {
153 | result += fmt.Sprintf("%#016x", binary.LittleEndian.Uint64(data[i:i+8])) + " "
154 | }
155 | } else {
156 | for i := 0; i < len(data); i += 8 {
157 | result += fmt.Sprintf("%#016x", binary.BigEndian.Uint64(data[i:i+8])) + " "
158 | }
159 | }
160 | break
161 | }
162 | result = strings.Trim(result, " ")
163 |
164 | return result
165 | }
166 |
167 | func asChar(data []byte) string {
168 | var result string = ""
169 | for _, b := range data {
170 | if b >= 32 && b <= 126 {
171 | result += fmt.Sprintf("%c", b)
172 | } else {
173 | result += "."
174 | }
175 | }
176 | return result
177 | }
178 |
179 | // Doesn't change the default formatting of the data
180 | func asRaw(data []byte) string {
181 | return fmt.Sprintf("%v", data)
182 | }
183 |
184 | // Adds some null bytes to the beggining of a slice
185 | func padBytesBeginning(data []byte, width int) []byte {
186 | if len(data)%width == 0 {
187 | return data
188 | }
189 |
190 | var bytesNeeded int = width - (len(data) % width)
191 |
192 | var result []byte
193 | for i := 0; i < bytesNeeded; i++ {
194 | result = append(result, 0)
195 | }
196 | result = append(result, data...)
197 | return result
198 | }
199 |
200 | // Add some null bytes to the end of a slice
201 | func padBytesEnd(data []byte, width int) []byte {
202 | if len(data)%width == 0 {
203 | return data
204 | }
205 |
206 | var bytesNeeded int = width - (len(data) % width)
207 |
208 | var result []byte
209 | for i := 0; i < bytesNeeded; i++ {
210 | result = append(result, 0)
211 | }
212 | result = append(data, result...)
213 | return result
214 | }
215 |
216 | // Adds padding to the bytes based on endianness
217 | func padBytes(data []byte, width int) []byte {
218 | if len(data)%width == 0 {
219 | return data
220 | }
221 |
222 | return padBytesEnd(data, width)
223 | }
224 |
225 | // Apply a format based on the specified format, width, endianness
226 | func applyFormat(format int, width int, endianness int, data []byte) string {
227 | if len(data) == 0 {
228 | return ""
229 | }
230 |
231 | data = padBytes(data, width)
232 |
233 | switch format {
234 | case Hex:
235 | return asHex(width, endianness, data)
236 | case Decimal:
237 | return asDecimal(width, endianness, data)
238 | case Char:
239 | return asChar(data)
240 | default:
241 | return asRaw(data)
242 | }
243 | }
244 |
245 | // Update the table view with the new map entries
246 | func (b *BpfMapTableView) updateTable() {
247 | b.table.Clear()
248 | b.table.SetCell(0, 0, tview.NewTableCell("Index").SetSelectable(false))
249 | b.table.SetCell(0, 1, tview.NewTableCell("Key").SetSelectable(false))
250 | b.table.SetCell(0, 2, tview.NewTableCell("Value").SetSelectable(false))
251 | for i, entry := range b.MapEntries {
252 | b.table.SetCell(i+1, 0, tview.NewTableCell(strconv.Itoa(i)))
253 | b.table.SetCell(i+1, 1, tview.NewTableCell(applyFormat(curFormat, curWidth, curEndianness, entry.Key)))
254 | b.table.SetCell(i+1, 2, tview.NewTableCell(applyFormat(curFormat, curWidth, curEndianness, entry.Value)))
255 | }
256 | }
257 |
258 | // Update Map
259 | func (b *BpfMapTableView) UpdateMap(m utils.BpfMap) error {
260 | var err error
261 | b.Map = m
262 |
263 | entries, err := utils.GetBpfMapEntries(b.Map.Id)
264 | if err != nil {
265 | b.app.DisplayError(fmt.Sprintf("Error getting map entries for map %d: %v\n", b.Map.Id, err))
266 | return err
267 | }
268 | b.MapEntries = entries
269 |
270 | b.updateTable()
271 | return nil
272 | }
273 |
274 | func (b *BpfMapTableView) buildMapTableView() {
275 | b.table.SetBorder(true).SetTitle("Map Info")
276 | b.table.SetSelectable(true, false)
277 | b.table.Select(1, 0)
278 | b.table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
279 |
280 | // If the user presses the esc key they should go back to the main view
281 | if event.Key() == tcell.KeyEsc {
282 | // app.SetFocus("main")
283 | return nil
284 | } else if event.Rune() == 'd' {
285 | b.pages.SwitchToPage("confirm")
286 | return nil
287 | }
288 | return event
289 | })
290 | b.table.SetSelectedFunc(func(row int, column int) {
291 | if len(b.MapEntries) <= 0 {
292 | return
293 | }
294 |
295 | key := b.MapEntries[row-1].Key
296 | value := b.MapEntries[row-1].Value
297 |
298 | keyPtr, ok := b.form.GetFormItemByLabel("Key").(*tview.InputField)
299 | if ok {
300 | keyPtr.SetText(fmt.Sprintf("%v", key))
301 | }
302 | valuePtr, ok := b.form.GetFormItemByLabel("Value").(*tview.InputField)
303 | if ok {
304 | valuePtr.SetText(fmt.Sprintf("%v", value))
305 | }
306 |
307 | b.form.SetFocus(0)
308 | b.pages.SwitchToPage("form")
309 | })
310 | }
311 |
312 | func cellTextToHexString(cellValue string) string {
313 | return strings.Trim(cellValue, "[]")
314 | }
315 |
316 | func cellTextToByteSlice(cellValue string) []byte {
317 | trimmed := strings.Trim(cellValue, "[]")
318 | split := strings.Split(trimmed, " ")
319 | var result []byte
320 | for _, s := range split {
321 | b, err := strconv.ParseUint(s, 0, 8)
322 | if err != nil {
323 | log.Printf("Error converting cell text to byte slice: %v\n", err)
324 | return []byte{}
325 | }
326 | result = append(result, byte(b))
327 | }
328 | return result
329 | }
330 |
331 | func (b *BpfMapTableView) buildMapTableEditForm() {
332 | var keyText string = ""
333 | var valueText string = ""
334 | b.form.AddInputField("Key", "", 0, nil, nil).
335 | AddInputField("Value", "", 0, nil, nil).
336 | AddButton("Save", func() {
337 | if len(b.MapEntries) <= 0 {
338 | return
339 | }
340 |
341 | // Get the new text value that the use input or the old one if they didn't change it
342 | keyPtr, ok := b.form.GetFormItemByLabel("Key").(*tview.InputField)
343 | if ok {
344 | keyText = keyPtr.GetText()
345 | }
346 | valuePtr, ok := b.form.GetFormItemByLabel("Value").(*tview.InputField)
347 | if ok {
348 | valueText = valuePtr.GetText()
349 | }
350 |
351 | cmd := strings.Split("sudo "+utils.BpftoolPath+" map update id "+strconv.Itoa(b.Map.Id)+" key "+cellTextToHexString(keyText)+" value "+cellTextToHexString(valueText), " ")
352 | _, _, err := utils.RunCmd(cmd...)
353 | if err != nil {
354 | if b.Map.Frozen == 1 {
355 | b.app.DisplayError("Failed to update map entry because the map is frozen")
356 | } else {
357 | b.app.DisplayError(fmt.Sprintf("Failed to update map entry: %v\nAttempted cmd: %s", err, cmd))
358 | }
359 | }
360 |
361 | // Update the map entries
362 | row, _ := b.table.GetSelection()
363 | b.MapEntries[row-1].Key = []byte(cellTextToByteSlice(keyText))
364 | b.MapEntries[row-1].Value = []byte(cellTextToByteSlice(valueText))
365 | b.UpdateMap(b.Map)
366 | b.pages.SwitchToPage("table")
367 | }).
368 | AddButton("Cancel", func() {
369 | b.pages.SwitchToPage("table")
370 | })
371 | }
372 |
373 | func (b *BpfMapTableView) buildConfirmModal() {
374 | b.confirm = tview.NewModal().
375 | SetText("Are you sure you want to delete this map entry?").
376 | AddButtons([]string{"Yes", "No"}).
377 | SetDoneFunc(func(buttonIndex int, buttonLabel string) {
378 | if buttonLabel == "Yes" {
379 | row, _ := b.table.GetSelection()
380 | key := strings.Trim(fmt.Sprintf("%v", b.MapEntries[row-1].Key), "[]")
381 | cmd := strings.Split("sudo "+utils.BpftoolPath+" map delete id "+strconv.Itoa(b.Map.Id)+" key "+key, " ")
382 | _, _, err := utils.RunCmd(cmd...)
383 | if err != nil {
384 | b.app.DisplayError(fmt.Sprintf("Error deleting map entry: %v\n", err))
385 | }
386 |
387 | b.UpdateMap(b.Map)
388 | }
389 | b.pages.SwitchToPage("table")
390 | })
391 | }
392 |
393 | func (b *BpfMapTableView) buildFilterForm() {
394 | b.filter = tview.NewForm().
395 | AddDropDown("Data Format", []string{"Hex", "Decimal", "Char", "Raw"}, 0, func(option string, optionIndex int) {
396 | switch optionIndex {
397 | case 0:
398 | curFormat = Hex
399 | break
400 | case 1:
401 | curFormat = Decimal
402 | break
403 | case 2:
404 | curFormat = Char
405 | break
406 | default:
407 | curFormat = Raw
408 | }
409 | b.updateTable()
410 | }).AddDropDown("Endianness", []string{"Little", "Big"}, 0, func(option string, optionIndex int) {
411 | switch optionIndex {
412 | case 0:
413 | curEndianness = Little
414 | break
415 | default:
416 | curEndianness = Big
417 | }
418 | b.updateTable()
419 | }).AddDropDown("Data Width", []string{"8", "16", "32", "64"}, 0, func(option string, optionIndex int) {
420 | switch optionIndex {
421 | case 0:
422 | curWidth = DataWidth8
423 | break
424 | case 1:
425 | curWidth = DataWidth16
426 | break
427 | case 2:
428 | curWidth = DataWidth32
429 | break
430 | default:
431 | curWidth = DataWidth64
432 | }
433 | b.updateTable()
434 | })
435 |
436 | }
437 |
438 | // Make a new BpfMapTableView. These functions only need to be called once
439 | func NewBpfMapTableView(tui *Tui) *BpfMapTableView {
440 | b := BpfMapTableView{
441 | form: tview.NewForm(),
442 | table: tview.NewTable(),
443 | pages: tview.NewPages(),
444 | app: tui,
445 | }
446 |
447 | b.buildMapTableView()
448 | b.buildMapTableEditForm()
449 | b.buildConfirmModal()
450 | b.buildFilterForm()
451 |
452 | flex := tview.NewFlex().
453 | AddItem(b.filter, 0, 1, false).
454 | AddItem(b.table, 0, 3, true)
455 |
456 | flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
457 | if event.Key() == tcell.KeyTAB {
458 | if b.table.HasFocus() {
459 | tui.App.SetFocus(b.filter)
460 | return nil
461 | }
462 | } else if event.Key() == tcell.KeyEsc {
463 | if b.filter.HasFocus() {
464 | tui.App.SetFocus(b.table)
465 | return nil
466 |
467 | }
468 | }
469 |
470 | return event
471 | })
472 |
473 | b.pages.AddPage("table", flex, true, true)
474 | b.pages.AddPage("form", b.form, true, false)
475 | b.pages.AddPage("confirm", b.confirm, true, false)
476 |
477 | return &b
478 | }
479 |
--------------------------------------------------------------------------------
/ui/map_test.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | // Compares the values of two slices to determine if they are equal
9 | func compareSlices(a []byte, b []byte) bool {
10 | if len(a) != len(b) {
11 | return false
12 | }
13 |
14 | for index, value := range a {
15 | if value != b[index] {
16 | return false
17 | }
18 | }
19 |
20 | return true
21 | }
22 |
23 | func TestPadBytes(t *testing.T) {
24 | widths := []int{DataWidth8, DataWidth16, DataWidth32, DataWidth64}
25 | for _, width := range widths {
26 | // If byte slice is empty we should always get a byte slice back
27 | result := padBytes([]byte{}, width)
28 | if len(result) != 0 {
29 | t.Errorf("Expected length of %d, got %d", width, len(result))
30 | }
31 |
32 | result = padBytes([]byte("A"), width)
33 | if len(result)%width != 0 {
34 | t.Errorf("Expected length of %d, got %d", width, len(result))
35 | }
36 |
37 | if strings.Index(string(result), "A") != 0 {
38 | t.Errorf("Expected 'A' at index 0, got '%s'", string(result))
39 | }
40 |
41 | // If byte slice has 5 values we should various amounts of padding
42 | result = padBytes([]byte("AAAAA"), width)
43 | if len(result)%width != 0 {
44 | t.Errorf("Expected length of %d, got %d", width, len(result))
45 | }
46 |
47 | switch width {
48 | case DataWidth8:
49 | case DataWidth16:
50 | case DataWidth32:
51 | if compareSlices(result, []byte("AAAAA")) {
52 | t.Errorf("Expected 'A' at index 0, got '%s'", string(result))
53 | }
54 | break
55 | case DataWidth64:
56 | if !compareSlices(result, []byte("AAAAA\x00\x00\x00")) {
57 | t.Errorf("Expected %v, got %v", []byte("AAAAA\x00\x00\x00"), result)
58 | }
59 | break
60 | }
61 |
62 | // If byte slice has 9 values we should various amounts of padding
63 | result = padBytes([]byte("AAAAAAAAA"), width)
64 | if len(result)%width != 0 {
65 | t.Errorf("Expected length of %d, got %d", width, len(result))
66 | }
67 |
68 | switch width {
69 | case DataWidth8:
70 | if !compareSlices(result, []byte("AAAAAAAAA")) {
71 | t.Errorf("Expected %v, got '%v'", []byte("AAAAAAAAA"), result)
72 | }
73 | break
74 | case DataWidth16:
75 | if !compareSlices(result, []byte("AAAAAAAAA\x00")) {
76 | t.Errorf("Expected %v, got %v", []byte("AAAAAAAAA\x00"), result)
77 | }
78 | break
79 | case DataWidth32:
80 | if !compareSlices(result, []byte("AAAAAAAAA\x00\x00\x00")) {
81 | t.Errorf("Expected %v, got %v", []byte("AAAAAAAAA\x00\x00\x00"), result)
82 | }
83 | break
84 | case DataWidth64:
85 | if !compareSlices(result, []byte("AAAAAAAAA\x00\x00\x00\x00\x00\x00\x00")) {
86 | t.Errorf("Expected %v, got %v", []byte("AAAAAAAAA\x00\x00\x00\x00\x00\x00\x00"), result)
87 | }
88 | break
89 | }
90 | }
91 | }
92 |
93 | func validResultOne(result string, format int, width int, endianness int) (string, bool) {
94 | // Declare map of valid values
95 | m := map[int]map[int]map[int]string{
96 | Hex: {
97 | DataWidth8: map[int]string{
98 | Little: "0x41",
99 | Big: "0x41",
100 | },
101 | DataWidth16: map[int]string{
102 | Little: "0x0041",
103 | Big: "0x4100",
104 | },
105 | DataWidth32: map[int]string{
106 | Little: "0x00000041",
107 | Big: "0x41000000",
108 | },
109 | DataWidth64: map[int]string{
110 | Little: "0x0000000000000041",
111 | Big: "0x4100000000000000",
112 | },
113 | },
114 | Decimal: {
115 | DataWidth8: map[int]string{
116 | Little: "65",
117 | Big: "65",
118 | },
119 | DataWidth16: map[int]string{
120 | Little: "65",
121 | Big: "16640",
122 | },
123 | DataWidth32: map[int]string{
124 | Little: "65",
125 | Big: "1090519040",
126 | },
127 | DataWidth64: map[int]string{
128 | Little: "65",
129 | Big: "4683743612465315840",
130 | },
131 | },
132 | Raw: {
133 | DataWidth8: map[int]string{
134 | Little: "[65]",
135 | Big: "[65]",
136 | },
137 | DataWidth16: map[int]string{
138 | Little: "[65 0]",
139 | Big: "[65 0]",
140 | },
141 | DataWidth32: map[int]string{
142 | Little: "[65 0 0 0]",
143 | Big: "[65 0 0 0]",
144 | },
145 | DataWidth64: map[int]string{
146 | Little: "[65 0 0 0 0 0 0 0]",
147 | Big: "[65 0 0 0 0 0 0 0]",
148 | },
149 | },
150 | }
151 | if result != m[format][width][endianness] {
152 | return m[format][width][endianness], false
153 | }
154 | return "", true
155 | }
156 |
157 | // Write a test that can test the applyFormat function
158 | // The test should handle all the variations of format,
159 | // width, and endianness along with different values
160 | func TestApplyFormat(t *testing.T) {
161 | formats := []int{Hex, Decimal, Raw}
162 | widths := []int{DataWidth8, DataWidth16, DataWidth32, DataWidth64}
163 | endiannesses := []int{Little, Big}
164 |
165 | // Test empty byte slice for all cases
166 | for _, format := range formats {
167 | for _, width := range widths {
168 | for _, endianness := range endiannesses {
169 | result := applyFormat(format, width, endianness, []byte{})
170 | if result != "" {
171 | t.Errorf("Expected empty string, got '%s'", result)
172 | }
173 | }
174 | }
175 | }
176 |
177 | for _, format := range formats {
178 | for _, width := range widths {
179 | for _, endianness := range endiannesses {
180 | result := applyFormat(format, width, endianness, []byte{0x41})
181 | expected, success := validResultOne(result, format, width, endianness)
182 | if !success {
183 | t.Errorf("(format, width, endian) %v, %v, %v, Expected %v, got %v", format, width, endianness, expected, result)
184 | }
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/ui/ui.go:
--------------------------------------------------------------------------------
1 | // This is the core code for building the TUI application portion of ebpfmon
2 | // This page handles building the root application and the pages that are used
3 | // to display each of the view the app supports. It also handles the global
4 | // keybindings and the global state of the application
5 | package ui
6 |
7 | import (
8 | "ebpfmon/utils"
9 | "fmt"
10 | "sync"
11 |
12 | log "github.com/sirupsen/logrus"
13 |
14 | "github.com/gdamore/tcell/v2"
15 | "github.com/rivo/tview"
16 | )
17 |
18 | var Programs map[int]utils.BpfProgram
19 | var BpftoolPath string
20 | var lock sync.Mutex
21 | var previousPage string
22 | var featureInfo string
23 |
24 | type FlowDissectorInfo struct {
25 | DevName string `json:"devname"`
26 | IfIndex int `json:"ifindex"`
27 | Id int `json:"id"`
28 | }
29 |
30 | type TcInfo struct {
31 | DevName string `json:"devname"`
32 | IfIndex int `json:"ifindex"`
33 | Kind string `json:"kind"`
34 | Name string `json:"name"`
35 | Id int `json:"id"`
36 | }
37 |
38 | type Tui struct {
39 | App *tview.Application
40 | pages *tview.Pages
41 | bpfExplorerView *BpfExplorerView
42 | bpfMapTableView *BpfMapTableView
43 | bpfFeatureview *BpfFeatureView
44 | helpView *HelpView
45 | errorView *ErrorView
46 | }
47 |
48 | func (t *Tui) DisplayError(err string) {
49 | log.Error(err)
50 | t.errorView.SetError(err)
51 | previousPage, _ = t.pages.GetFrontPage()
52 | t.pages.SwitchToPage("error")
53 | }
54 |
55 | func NewTui(bpftoolPath string) *Tui {
56 | Programs = map[int]utils.BpfProgram{}
57 | BpftoolPath = bpftoolPath
58 |
59 | // Initialize the global page manager and the application
60 | app := NewApp()
61 | pages := tview.NewPages()
62 | tui := &Tui{App: app, pages: pages}
63 |
64 | // Create each page object. Each object gets a reference to the main application
65 | tui.bpfExplorerView = NewBpfExplorerView(tui)
66 | tui.bpfFeatureview = NewBpfFeatureView(tui)
67 | tui.bpfMapTableView = NewBpfMapTableView(tui)
68 | tui.helpView = NewHelpView()
69 | tui.errorView = NewErrorView()
70 |
71 | fmt.Println("Collecting bpf information. This may take a few seconds")
72 | updateBpfPrograms()
73 |
74 | // Set up proper page navigation and global quit key
75 | // In page navigation happens in their respective files
76 | app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
77 | // Set up q quit key and page navigation
78 | if event.Rune() == 'q' || event.Rune() == 'Q' {
79 | app.Stop()
80 | return nil
81 | } else if event.Key() == tcell.KeyCtrlE {
82 | page, _ := pages.GetFrontPage()
83 | if page != "help" {
84 | previousPage = page
85 | }
86 | pages.SwitchToPage("programs")
87 | _, prim := pages.GetFrontPage()
88 | app.SetFocus(prim)
89 | return nil
90 | } else if event.Key() == tcell.KeyCtrlF {
91 | page, _ := pages.GetFrontPage()
92 | if page != "help" {
93 | previousPage = page
94 | }
95 |
96 | pages.SwitchToPage("features")
97 |
98 | // Set focus to the input field
99 | app.SetFocus(tui.bpfFeatureview.flex.GetItem(0))
100 | return nil
101 | } else if event.Key() == tcell.KeyF1 || event.Rune() == '?' {
102 | name, _ := pages.GetFrontPage()
103 | if name == "help" {
104 | pages.SwitchToPage(previousPage)
105 | } else {
106 | page, _ := pages.GetFrontPage()
107 | if page != "help" {
108 | previousPage = page
109 | }
110 | pages.SwitchToPage("help")
111 | _, prim := pages.GetFrontPage()
112 | app.SetFocus(prim)
113 | }
114 | return nil
115 | } else if event.Key() == tcell.KeyESC {
116 | name, prim := pages.GetFrontPage()
117 | if name == "maptable" {
118 | return event
119 | }
120 | pages.SwitchToPage(previousPage)
121 | name, prim = pages.GetFrontPage()
122 | app.SetFocus(prim)
123 | return nil
124 | }
125 | return event
126 | })
127 |
128 | // These are the main pages for the application
129 | pages.AddPage("programs", tui.bpfExplorerView.flex, true, true)
130 | pages.AddPage("help", tui.helpView.modal, true, false)
131 | pages.AddPage("features", tui.bpfFeatureview.flex, true, false)
132 | pages.AddPage("maptable", tui.bpfMapTableView.pages, true, false)
133 | pages.AddPage("error", tui.errorView.modal, true, false)
134 |
135 | // Set starting page as previous page
136 | previousPage = "programs"
137 |
138 | // Set the page view as the root
139 | app.SetRoot(pages, true)
140 |
141 | // Start the go routine to update bpf programs and maps
142 | go tui.bpfExplorerView.Update()
143 |
144 | return tui
145 | }
146 |
147 | func NewApp() *tview.Application {
148 | app := tview.NewApplication()
149 | return app
150 | }
151 |
--------------------------------------------------------------------------------
/utils/bpf.go:
--------------------------------------------------------------------------------
1 | // The utils/bpf.go file is for implementing code that handles some of the
2 | // specifics for getting information about bpf programs and maps. This includes
3 | // parsing the output of the bpftool binary.
4 | package utils
5 |
6 | import (
7 | "encoding/hex"
8 | "encoding/json"
9 | "errors"
10 | "fmt"
11 | log "github.com/sirupsen/logrus"
12 | "strconv"
13 | "strings"
14 | )
15 |
16 | var BpftoolPath string
17 |
18 | type ProcessInfo struct {
19 | Pid int `json:"pid"`
20 | Comm string `json:"comm"`
21 | Cmdline string
22 | Path string
23 | Uid int
24 | Gid int
25 | }
26 |
27 | type BpfMap struct {
28 | // The id of the map
29 | Id int `json:"id"`
30 |
31 | // The type of map
32 | Type string `json:"type"`
33 |
34 | // The name of the map if present
35 | Name string `json:"name",omitempty`
36 |
37 | // Any flags that are set on the map
38 | Flags int `json:"flags"`
39 |
40 | // The key size (in bytes)
41 | KeySize int `json:"bytes_key"`
42 |
43 | // The value size (in bytes)
44 | ValueSize int `json:"bytes_value"`
45 |
46 | // The max number of entries in the map
47 | MaxEntries int `json:"max_entries"`
48 |
49 | // The amount of memory the map lockes in
50 | Memlock int `json:"bytes_memlock"`
51 |
52 | // The btf id referenced by the map
53 | BtfId int `json:"btf_id",omitempty`
54 |
55 | // The state of the map. Examples could be frozen, pinned etc
56 | Frozen int `json:"frozen",omitempty`
57 |
58 | // If the map is pinned the path will be here
59 | Pinned []string `json:"pinned",omitempty`
60 | }
61 |
62 | type BpfMapEntryRaw struct {
63 | // The key of the map entry
64 | Key []string `json:"key"`
65 |
66 | // The value of the map entry
67 | Value []string `json:"value"`
68 |
69 | // The formatted value of the map entry if it exists
70 | Formatted struct {
71 | // Value can be a variety of things
72 | Value interface{} `json:"value"`
73 | } `json:"formatted",omitempty`
74 | }
75 |
76 | type BpfMapEntry struct {
77 | // The key of the map entry
78 | Key []byte `json:"key"`
79 |
80 | // The value of the map entry
81 | Value []byte `json:"value"`
82 |
83 | // The formatted value of the map entry if it exists
84 | Formatted struct {
85 | // Value can be a variety of things
86 | Value interface{} `json:"value"`
87 | } `json:"formatted",omitempty`
88 | }
89 |
90 | type BpfProgram struct {
91 | // The name of the program. This field may be empty
92 | Name string `json:"name",omitempty`
93 |
94 | // The tag of the program. This field should not be empty
95 | Tag string `json:"tag"`
96 |
97 | // The bpf program id
98 | ProgramId int `json:"id"`
99 |
100 | // The type of the program i.e. Kprobe, Kretprobe, Uprobe etc
101 | ProgType string `json:"type"`
102 |
103 | // The license of the program. May be empty
104 | GplCompatible bool `json:"gpl_compatible"`
105 |
106 | // The time the program was loaded
107 | LoadedAt int `json:"loaded_at"`
108 |
109 | // The uid of the owner
110 | OwnerUid int `json:"uid"`
111 |
112 | // The number of instructions in the xlated version of the program
113 | BytesXlated int `json:"bytes_xlated"`
114 |
115 | // Whether or not the program is jited
116 | Jited bool `json:"jited"`
117 |
118 | // The number of jited instructions
119 | BytesJited int `json:"bytes_jited"`
120 |
121 | // The amount of memory that is locked
122 | BytesMemlock int `json:"bytes_memlock"`
123 |
124 | // The ids of any maps the program references
125 | MapIds []int `json:"map_ids",omitempty`
126 |
127 | // The id of an btf objects the program references
128 | BtfId int `json:"btf_id",omitempty`
129 |
130 | // If the program is pinned this field will contain the path
131 | Pinned []string `json:"pinned",omitempty`
132 |
133 | Pids []ProcessInfo `json:"pids"`
134 |
135 | // The attach points of the program. There may be multiple
136 | AttachPoint []string
137 |
138 | // The offset from the attach point
139 | Offset int
140 |
141 | // The fd of the bpf program
142 | Fd int
143 |
144 | // A sha256 hash representing a unique id for the program
145 | Fingerprint []string
146 |
147 | // The disassembly of the program
148 | Instructions []string
149 |
150 | // The network interface this program is attached to
151 | Interface string
152 |
153 | // The type of TC program
154 | TcKind string
155 |
156 | // Cgroup that the program is attached to
157 | Cgroup string
158 |
159 | // The attach type for the cgroup. Examples are ingress, egress, device, bind4, bind6 etc
160 | CgroupAttachType string
161 |
162 | // Either multi or override
163 | CgroupAttachFlags string
164 | }
165 |
166 | // Write a stringer for BpfProgram
167 | func (p BpfProgram) String() string {
168 | result := fmt.Sprintf("%6d: [green]%13s[-] [blue]%16s[-] %20s ", p.ProgramId, p.ProgType, p.Tag, p.Name)
169 | for _, point := range p.AttachPoint {
170 | result += fmt.Sprintf("%s, ", point)
171 | }
172 | result = strings.TrimSuffix(result, ", ")
173 | return result
174 | }
175 |
176 | // Write a stringer for BpfMap
177 | func (p BpfMap) String() string {
178 | result := fmt.Sprintf("[blue]%7d:[-] %s", p.Id, p.Type)
179 | if p.Name != "" {
180 | result += p.Name
181 | }
182 |
183 | if p.Frozen == 1 {
184 | result += " [yellow](frozen)[-]"
185 | }
186 | return result
187 | }
188 |
189 | // Write a stringer for BpfMapEntry
190 | func (e BpfMapEntry) String() string {
191 | result := fmt.Sprintf("%v: %v", e.Key, e.Value)
192 | return result
193 | }
194 |
195 | // Call the bpftool binary to get the disassembly of a program
196 | // using the program id
197 | func GetBpfProgramDisassembly(programId int) ([]string, error) {
198 | stdout, _, err := RunCmd("sudo", BpftoolPath, "prog", "dump", "xlated", "id", strconv.Itoa(programId))
199 | if err != nil {
200 | return []string{}, err
201 | }
202 | // Convert the output to a string
203 | outStr := string(stdout)
204 | // Split the output into lines
205 | result := strings.Split(outStr, "\n")
206 | return result, nil
207 | }
208 |
209 | // Use bpftool map show to get the map info
210 | func GetBpfMapInfo() ([]BpfMap, error) {
211 | var bpfMap []BpfMap
212 | stdout, _, err := RunCmd("sudo", BpftoolPath, "map", "-jf", "show")
213 | if err != nil {
214 | log.Errorf("Error getting map info: %v\n", err)
215 | return bpfMap, err
216 | }
217 |
218 | err = json.Unmarshal(stdout, &bpfMap)
219 | if err != nil {
220 | log.Errorf("Error unmarshalling map info: %v\n", err)
221 | return bpfMap, err
222 | }
223 | return bpfMap, nil
224 | }
225 |
226 | // Parse the output of the bpftool binary to get the map info that correspond
227 | // to the map ids the bpf program is using. It assumed that at least one of
228 | // the ids should exist so finding none is considered an error
229 | func GetBpfMapInfoByIds(mapIds []int) ([]BpfMap, error) {
230 | tmp := []BpfMap{}
231 | result := []BpfMap{}
232 |
233 | // Call the bpftool binary to get the map info
234 | stdout, _, err := RunCmd("sudo", BpftoolPath, "-j", "map", "show")
235 | if err != nil {
236 | log.Errorf("Error getting map info for ids: %v\n%v\n", mapIds, err)
237 | return []BpfMap{}, err
238 | }
239 | err = json.Unmarshal(stdout, &tmp)
240 | if err != nil {
241 | log.Errorf("Error unmarshalling map info for ids: %v\n%v\n", mapIds, err)
242 | return []BpfMap{}, err
243 | }
244 |
245 | for _, m := range tmp {
246 | if contains(mapIds, m.Id) {
247 | result = append(result, m)
248 | }
249 | }
250 | if len(result) == 0 {
251 | log.Errorf("No map info found for ids: %v\n", mapIds)
252 | return []BpfMap{}, errors.New("No map info found")
253 | }
254 | return result, nil
255 | }
256 |
257 | func convertStringSliceToByteSlice(strSlice []string) ([]byte, error) {
258 |
259 | byteSlice := make([]byte, len(strSlice))
260 |
261 | for i, str := range strSlice {
262 | // Remove the "0x" prefix from the string
263 | if len(str) >= 2 && str[:2] == "0x" {
264 | str = str[2:]
265 | }
266 |
267 | // Parse the string as a hexadecimal value
268 | bytes, err := hex.DecodeString(str)
269 | if err != nil {
270 | log.Errorf("Error decoding string slice to byte slice: %v\n", err)
271 | return []byte{}, err
272 | }
273 |
274 | // Append the byte value to the byte slice
275 | byteSlice[i] = bytes[0]
276 | }
277 |
278 | return byteSlice, nil
279 | }
280 |
281 | // Use bpftool map dump to get the data from a map
282 | func GetBpfMapEntries(mapId int) ([]BpfMapEntry, error) {
283 | var result []BpfMapEntry
284 | var mapData []BpfMapEntryRaw
285 | stdout, _, err := RunCmd("sudo", BpftoolPath, "map", "-jf", "dump", "id", strconv.Itoa(mapId))
286 | if err != nil {
287 | log.Errorf("Error getting map entries for map id: %d\n%v\n", mapId, err)
288 | return result, err
289 | }
290 |
291 | // Convert map data to individual elements
292 | err = json.Unmarshal(stdout, &mapData)
293 | if err != nil {
294 | log.Errorf("Error unmarshalling map entries for map id: %d\n%v\n", mapId, err)
295 | return result, err
296 | }
297 |
298 | // Use hex.DecodeString to convert the key and value to byte slices
299 | for i, _ := range mapData {
300 | b, err := convertStringSliceToByteSlice(mapData[i].Key)
301 | if err != nil {
302 | return []BpfMapEntry{}, err
303 | }
304 |
305 | v, err := convertStringSliceToByteSlice(mapData[i].Value)
306 | if err != nil {
307 | return []BpfMapEntry{}, err
308 | }
309 |
310 | result = append(result, BpfMapEntry{Key: b, Value: v})
311 | }
312 |
313 | return result, nil
314 | }
315 |
--------------------------------------------------------------------------------
/utils/bpf_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "math/rand"
5 | "os"
6 | "path/filepath"
7 | "strconv"
8 | "testing"
9 |
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | func init() {
14 | // Set the path to bpftool using the system path
15 | bpftoolEnvPath, exists := os.LookupEnv("BPFTOOL_PATH")
16 | if exists {
17 | _, err := os.Stat(bpftoolEnvPath)
18 | if err != nil {
19 | panic(err)
20 | }
21 | bpftoolEnvPath, err = filepath.Abs(bpftoolEnvPath)
22 | if err != nil {
23 | panic(err)
24 | }
25 | BpftoolPath = bpftoolEnvPath
26 | } else {
27 | path, err := Which("bpftool")
28 | if err != nil {
29 | panic(err)
30 | }
31 | BpftoolPath = path
32 | }
33 |
34 | // Set simple logging for tests
35 | log.SetOutput(os.Stdout)
36 | log.SetLevel(log.WarnLevel)
37 | }
38 |
39 | // Compares the values of two slices to determine if they are equal
40 | func compareSlices(a []byte, b []byte) bool {
41 | if len(a) != len(b) {
42 | return false
43 | }
44 |
45 | for index, value := range a {
46 | if value != b[index] {
47 | return false
48 | }
49 | }
50 |
51 | return true
52 | }
53 |
54 | // Tests the ability for this function to convert a string slice to a byte slice
55 | // The string is expected to be a hex string i.e. "0x00"
56 | func TestConvertStringSliceToByteSlice(t *testing.T) {
57 | // Create a few test strings with hex values
58 | testStringEmpty := []string{}
59 | testStringOne := []string{"0x00"}
60 | testStringTwo := []string{"0x00", "0x01"}
61 | testStrings := []string{"0x00", "0x01", "0x02", "0x03", "0x04", "0x05"}
62 |
63 | // Test empty string
64 | result, err := convertStringSliceToByteSlice(testStringEmpty)
65 | if err != nil {
66 | t.Errorf("Expected nil error, got %v", err)
67 | }
68 | if len(result) != 0 {
69 | t.Errorf("Expected length of 0, got %d", len(result))
70 | }
71 |
72 | // Test one string
73 | result, err = convertStringSliceToByteSlice(testStringOne)
74 | if err != nil {
75 | t.Errorf("Expected nil error, got %v", err)
76 | }
77 | if !compareSlices(result, []byte{0x00}) {
78 | t.Errorf("Expected [0x00], got %v", result)
79 | }
80 |
81 | // Test two strings
82 | result, err = convertStringSliceToByteSlice(testStringTwo)
83 | if err != nil {
84 | t.Errorf("Expected nil error, got %v", err)
85 | }
86 | if !compareSlices(result, []byte{0x00, 0x01}) {
87 | t.Errorf("Expected [0x00, 0x01], got %v", result)
88 | }
89 |
90 | // Convert the test strings to a byte slice
91 | result, err = convertStringSliceToByteSlice(testStrings)
92 | if err != nil {
93 | t.Errorf("Expected nil error, got %v", err)
94 | }
95 | if !compareSlices(result, []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}) {
96 | t.Errorf("Expected [0x00, 0x01, 0x02, 0x03, 0x04, 0x05], got %v", result)
97 | }
98 |
99 | }
100 |
101 | // Generate a random string of a given length
102 | func generateRandomString(length int) string {
103 | var result string
104 | for i := 0; i < length; i++ {
105 | result += strconv.Itoa(rand.Intn(10))
106 | }
107 | return result
108 | }
109 |
110 | func TestGetMapEntries(t *testing.T) {
111 | sysfsPath := "/sys/fs/bpf"
112 | mapName := generateRandomString(10)
113 | mapPinPath := sysfsPath + "/" + mapName
114 |
115 | // Create a new bpf map using bpftool
116 | _, stderr, err := RunCmd("sudo", "bpftool", "map", "create", mapPinPath, "type", "hash", "key", "4", "value", "4", "entries", "1024", "name", mapName)
117 | if err != nil {
118 | t.Errorf("Failed to create map at %s, got %v - %s", mapPinPath, err, stderr)
119 | return
120 | }
121 |
122 | // Get the id of the map
123 | maps, err := GetBpfMapInfo()
124 | if err != nil {
125 | t.Errorf("Failed to get bpf map info, got %v", err)
126 | return
127 | }
128 |
129 | // Find the map id by matching the map name
130 | var mapId int = 0
131 | for _, m := range maps {
132 | if m.Name == mapName {
133 | mapId = m.Id
134 | }
135 | }
136 |
137 | if mapId == 0 {
138 | t.Errorf("Failed to find map id for map %s", mapName)
139 | return
140 | }
141 |
142 | // Add entires to the map using bpftool map update
143 | _, _, err = RunCmd("sudo", "bpftool", "map", "update", "id", strconv.Itoa(mapId), "key", "0x00", "0x00", "0x00", "0x00", "value", "0x00", "0x00", "0x00", "0x00")
144 |
145 | // Get the map entries
146 | entries, err := GetBpfMapEntries(mapId)
147 | if err != nil {
148 | t.Errorf("Failed to get map entries, got %v", err)
149 | return
150 | }
151 |
152 | // Check that the map entries are correct
153 | if len(entries) != 1 {
154 | t.Errorf("Expected 1 entry, got %d", len(entries))
155 | return
156 | }
157 | if !compareSlices(entries[0].Key, []byte{0x00, 0x00, 0x00, 0x00}) {
158 | t.Errorf("Expected key [0x00, 0x00, 0x00, 0x00], got %v", entries[0].Key)
159 | return
160 | }
161 | if !compareSlices(entries[0].Value, []byte{0x00, 0x00, 0x00, 0x00}) {
162 | t.Errorf("Expected value [0x00, 0x00, 0x00, 0x00], got %v", entries[0].Value)
163 | return
164 | }
165 |
166 | // Delete the map
167 | // _, _, err = RunCmd("sudo", "bpftool", "map", "delete", "id", strconv.Itoa(mapId))
168 | // if err != nil {
169 | // t.Errorf("Failed to delete map, got %v", err)
170 | // }
171 |
172 | // Delete the map pin
173 | _, _, err = RunCmd("sudo", "rm", "-rf", mapPinPath)
174 | if err != nil {
175 | t.Errorf("Failed to delete map pin, got %v", err)
176 | return
177 | }
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | // Generic utility functions used by other packages
2 | package utils
3 |
4 | import (
5 | "bytes"
6 | "fmt"
7 | "io/fs"
8 | "os"
9 | "os/exec"
10 | "path/filepath"
11 | "regexp"
12 | "strconv"
13 | "strings"
14 | )
15 |
16 | type CgroupProcNotFoundError struct{}
17 |
18 | func (m *CgroupProcNotFoundError) Error() string {
19 | return "No cgroup.procs file found or no processes in cgroup"
20 | }
21 |
22 | type CgroupProcess struct {
23 | // The pid of a process in a cgroup
24 | Pid int
25 |
26 | // The path to the cgroup the process is in
27 | CgroupPath string
28 |
29 | // The comm string of the process in the cgroup
30 | Comm string
31 |
32 | // The cmdline of the process in the cgroup
33 | Cmdline string
34 |
35 | // The path of the executable of the process in the cgroup
36 | Path string
37 | }
38 |
39 | // Get the name of a process using the pid
40 | func GetProcessName(pid int) (string, error) {
41 | cmd := exec.Command("sudo", "ps", "-p", strconv.Itoa(pid), "-o", "comm=")
42 | out, err := cmd.Output()
43 | if err != nil {
44 | return "", err
45 | }
46 | // Convert the output to a string
47 | outStr := string(out)
48 | // Trim the newline
49 | outStr = strings.TrimSuffix(outStr, "\n")
50 | return outStr, nil
51 | }
52 |
53 | // Get the path of a process using the pid
54 | func GetProcessPath(pid int) (string, error) {
55 | cmd := exec.Command("sudo", "readlink", "-f", "/proc/"+strconv.Itoa(pid)+"/exe")
56 | out, err := cmd.Output()
57 | if err != nil {
58 | return "", err
59 | }
60 | // Convert the output to a string
61 | outStr := string(out)
62 | // Trim the newline
63 | outStr = strings.TrimSuffix(outStr, "\n")
64 | return outStr, nil
65 | }
66 |
67 | // Get the processes inside of a given cgroup
68 | func GetProcsInCgroup(cgroupPath string) ([]CgroupProcess, error) {
69 | var result []CgroupProcess
70 | var comm string = ""
71 | var cmdline string = ""
72 | var path string = ""
73 | var tmp string = ""
74 |
75 | // Read the tasks file to get the list of processes
76 | tasks, err := os.ReadFile(filepath.Join(cgroupPath, "cgroup.procs"))
77 | if err != nil {
78 | return []CgroupProcess{}, err
79 | }
80 |
81 | // Convert the tasks to a list of strings
82 | processes := strings.Split(strings.TrimSpace(string(tasks)), "\n")
83 |
84 | if len(processes) == 1 && processes[0] == "" {
85 | return []CgroupProcess{}, &CgroupProcNotFoundError{}
86 | }
87 |
88 | for _, procId := range processes {
89 | procId, err := strconv.Atoi(procId)
90 | if err != nil {
91 | return []CgroupProcess{}, err
92 | }
93 | tmp, err = GetProcessName(procId)
94 | if err != nil {
95 | comm = ""
96 | } else {
97 | comm = tmp
98 | }
99 |
100 | tmp, err = GetProcessCmdline(procId)
101 | if err != nil {
102 | cmdline = ""
103 | } else {
104 | cmdline = tmp
105 | }
106 |
107 | tmp, err = GetProcessPath(procId)
108 | if err != nil {
109 | path = ""
110 | } else {
111 | path = tmp
112 | }
113 |
114 | result = append(result, CgroupProcess{
115 | Pid: procId,
116 | CgroupPath: cgroupPath,
117 | Comm: comm,
118 | Cmdline: cmdline,
119 | Path: path,
120 | })
121 | }
122 | return result, nil
123 | }
124 |
125 | // Get a list of processes in a cgroup. The cgroup path is optional and defaults
126 | // to /sys/fs/cgroup
127 | func parseCgroups(path ...string) (map[string][]string, error) {
128 | cgroups := make(map[string][]string)
129 |
130 | // Walk the cgroups directory recursively
131 | cgroupPath := "/sys/fs/cgroup"
132 | if len(path) == 1 {
133 | cgroupPath = path[0]
134 | }
135 | err := filepath.Walk(cgroupPath, func(path string, info os.FileInfo, err error) error {
136 | if err != nil {
137 | return err
138 | }
139 |
140 | // Only process directories with a "tasks" file
141 | if info.IsDir() && FileExists(filepath.Join(path, "cgroup.procs")) {
142 | // Read the tasks file to get the list of processes
143 | tasks, err := os.ReadFile(filepath.Join(path, "cgroup.procs"))
144 | if err != nil {
145 | return err
146 | }
147 |
148 | // Convert the tasks to a list of strings
149 | processes := strings.Split(strings.TrimSpace(string(tasks)), "\n")
150 |
151 | if len(processes) == 1 && processes[0] == "" {
152 | return nil
153 | }
154 |
155 | // Build the cgroup path by removing the "/sys/fs/cgroup/" prefix from the path
156 | cgroup := strings.TrimPrefix(path, "/sys/fs/cgroup/")
157 |
158 | // Add the cgroup and its processes to the map
159 | cgroups[cgroup] = processes
160 | }
161 |
162 | return nil
163 | })
164 |
165 | if err != nil {
166 | return nil, err
167 | }
168 |
169 | return cgroups, nil
170 | }
171 |
172 | // Get the command line of a process using the pid
173 | func GetProcessCmdline(procId int) (string, error) {
174 | cmdlineBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", procId))
175 | if err != nil {
176 | return "", err
177 | }
178 | cmdline := strings.ReplaceAll(string(cmdlineBytes), "\x00", " ")
179 | return cmdline, err
180 | }
181 |
182 | func isNumeric(str string) bool {
183 | _, err := strconv.Atoi(str)
184 | return err == nil
185 | }
186 |
187 | // Check if a file exists
188 | func FileExists(path string) bool {
189 | _, err := os.Stat(path)
190 | return err == nil
191 | }
192 |
193 | // Simple wrapper for running a command and returning the stdout and stderr
194 | func RunCmd(args ...string) ([]byte, []byte, error) {
195 | cmd := exec.Command(args[0], args[1:]...)
196 | var stdout, stderr bytes.Buffer
197 | cmd.Stdout = &stdout
198 | cmd.Stderr = &stderr
199 | err := cmd.Run()
200 | return stdout.Bytes(), stderr.Bytes(), err
201 | }
202 |
203 | func FindProcByProgId(progId int) ([]int, error) {
204 | var pids []int
205 | id := strconv.Itoa(progId)
206 |
207 | // Walk the /proc directory
208 | err := filepath.WalkDir("/proc", func(path string, info fs.DirEntry, err error) error {
209 | if err != nil {
210 | return nil
211 | }
212 |
213 | // Only process directories with a numeric name (i.e., a process ID)
214 | if info.IsDir() && isNumeric(info.Name()) {
215 | pid, err := strconv.Atoi(info.Name())
216 | if err != nil {
217 | // fmt.Printf("Failed to convert %s\n", info.Name())
218 | return nil
219 | }
220 |
221 | // Read the files in the /proc//fdinfo directory
222 | fdinfoDir := filepath.Join(path, "fdinfo")
223 | if FileExists(fdinfoDir) && !strings.Contains(fdinfoDir, "task") {
224 | filesSlice := []string{}
225 | files, err := os.ReadDir(fdinfoDir)
226 | for _, file := range files {
227 | filesSlice = append(filesSlice, filepath.Join(fdinfoDir, file.Name()))
228 | }
229 |
230 | if err != nil {
231 | // fmt.Printf("Failed to read%s\n", fdinfoDir)
232 | return nil
233 | }
234 |
235 | // Check each file for the prog_id string
236 | for _, file := range filesSlice {
237 | // fmt.(file)
238 |
239 | content, err := os.ReadFile(file)
240 | if err != nil {
241 | // fmt.Printf("Failed to read fdinfo file %s\n", file)
242 | continue
243 | }
244 |
245 | // Write a regex for detecting the prog_id string
246 | // The pattern looks like this:
247 | // prog_id: 1
248 | r := regexp.MustCompile(`prog_id:\s+\d+`)
249 | match := r.FindString(string(content))
250 | if match != "" {
251 | curProgid := strings.TrimSpace(strings.Split(match, ":")[1])
252 | if curProgid == id {
253 | pids = append(pids, pid)
254 | break
255 | }
256 | }
257 | }
258 | }
259 | }
260 |
261 | return nil
262 | })
263 |
264 | if err != nil {
265 | // fmt.Println("Failed")
266 | return nil, err
267 | }
268 |
269 | return pids, nil
270 | }
271 |
272 | // Write a function that checks if an int is in a slice
273 | func contains(s []int, e int) bool {
274 | for _, a := range s {
275 | if a == e {
276 | return true
277 | }
278 | }
279 | return false
280 | }
281 |
282 | // Write a function with a receiver to compare two BpfProgram structs
283 | func (p BpfProgram) Compare(other BpfProgram) bool {
284 | if p.ProgramId == other.ProgramId &&
285 | p.Tag == other.Tag {
286 | return true
287 | }
288 | return false
289 | }
290 |
291 | // Tview uses a special syntax in strings to colorize things. This function
292 | // removes those color codes from a string
293 | // And example string would look like "[blue]mystring[-]"
294 | func RemoveStringColors(s string) string {
295 | if s == "" {
296 | return s
297 | }
298 |
299 | // Write a regex to match the color codes
300 | r := regexp.MustCompile(`\[\w+\]|\[-\]`)
301 | // Replace the color codes with an empty string
302 | result := r.ReplaceAllString(s, "")
303 | return result
304 | }
305 |
306 | func Which(program string) (string, error) {
307 | path, err := exec.LookPath("bpftool")
308 | if err != nil {
309 | fmt.Println("Failed to find compiled version of bpftool")
310 | return "", err
311 | } else {
312 | path, err = filepath.Abs(path)
313 | if err != nil {
314 | fmt.Println("Failed to find compiled version of bpftool")
315 | return "", err
316 | }
317 | }
318 | return path, nil
319 | }
320 |
--------------------------------------------------------------------------------
/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | // Tests the RemoveStringColors function
8 | func TestRemoveStringColors(t *testing.T) {
9 | // Create a string with colors
10 | str := "[blue]my string[-]"
11 |
12 | // Remove the colors
13 | result := RemoveStringColors(str)
14 | if result != "my string" {
15 | t.Errorf("Expected 'my string', got '%s'", result)
16 | }
17 |
18 | // Create an empty string with colors
19 | str = "[red][-]"
20 | result = RemoveStringColors(str)
21 | if result != "" {
22 | t.Errorf("Expected '', got '%s'", result)
23 | }
24 |
25 | // Test the empty string
26 | str = ""
27 | result = RemoveStringColors(str)
28 | if result != "" {
29 | t.Errorf("Expected '', got '%s'", result)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------