├── .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 | ![Build workflow](https://github.com/redcanaryco/ebpfmon/actions/workflows/build.yml/badge.svg) 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 | --------------------------------------------------------------------------------