├── .ccls ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── Pipfile ├── README.md ├── bin └── pybpf ├── pybpf ├── __init__.py ├── bootstrap.py ├── cli │ ├── __init__.py │ ├── aliased_group.py │ ├── pybpf_compile.py │ ├── pybpf_gen.py │ └── pybpf_init.py ├── lib.py ├── maps.py ├── programs.py ├── skeleton.py ├── syscall.py ├── templates │ ├── bpf │ │ ├── prog.bpf.c │ │ ├── prog.bpf.h │ │ └── pybpf.bpf.h │ └── bpf_program.py ├── utils.py └── version.py ├── requirements.txt ├── setup.py └── tests ├── .gitignore ├── bpf_src ├── .gitignore ├── cgroup.bpf.c ├── hello.bpf.c ├── maps.bpf.c ├── prog.bpf.c ├── pybpf.bpf.h ├── ringbuf.bpf.c └── xdp.bpf.c ├── conftest.py ├── test_maps.py └── test_progs.py /.ccls: -------------------------------------------------------------------------------- 1 | clang 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: willfindlay 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected Behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Minimal Reproduction** 17 | Minimal steps to reproduce the bug. For example, a minimal BPF program and userspace script. 18 | 19 | **Configuration (please complete the following information):** 20 | - Linux Distribution: [e.g. Arch] 21 | - Kernel Version [e.g. 5.8.1-arch-1] 22 | - Relevant Kernel Configuration [e.g. CONFIG_BPF=y, etc.] 23 | - PyBPF Version [e.g. 0.0.1] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Pipfile.lock 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pybpf/libbpf"] 2 | path = pybpf/libbpf 3 | url = https://github.com/libbpf/libbpf 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | msg = @printf ' %-8s %s\n' "$(1)" "$(if $(2), $(notdir $(2)))"; 2 | 3 | .PHONY: dev 4 | dev: 5 | $(call msg,INSTALL,pip3 install -e .) 6 | @sudo su -c "pip3 install -e ." 7 | 8 | .PHONY: test 9 | test: 10 | sudo pytest -v -ra 11 | 12 | .PHONY: libbpf 13 | libbpf: 14 | git submodule update 15 | $(MAKE) -c pybpf/libbpf -j$(shell nproc) 16 | sudo su -c "$(MAKE) -c pybpf/libbpf install" 17 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pyroute2 = "*" 8 | jedi = "*" 9 | pylint = "*" 10 | pytest = "*" 11 | wrapt = {editable = true, path = "."} 12 | 13 | [packages] 14 | click = "*" 15 | 16 | [requires] 17 | python_version = "3.8" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyBPF 🐍💞🐝 2 | 3 | An opinionated libbpf/BPF CO-RE (Compile Once---Run Everywhere) library for the Python3 (read Python 3.6+) ecosystem. 4 | 5 | **DISCLAIMER:** This library is in **pre-alpha** version and changes may result in 6 | API breakages. Pre-release version API breakages will be accompanied by a minor 7 | version number bump. Versions post 1.0.0 will follow semantic versioning and 8 | breakages will require a bump in major version number. 9 | 10 | ## Compilation Requirements 11 | 12 | - Latest libbpf (https://github.com/libbpf/libbpf) 13 | - Linux kernel compiled with BTF debug info 14 | - bpftool (available from under linux/tools/bpf/bpftool from official kernel repositories) 15 | - Clang/LLVM 10+ 16 | - gcc 17 | - Python 3.6+ 18 | 19 | ## Deployment Requirements 20 | 21 | - A pre-compiled shared library for your BPF program, generated with PyBPF (see requirements above) 22 | - Linux kernel compiled with BTF debug info 23 | - Python 3.6+ 24 | 25 | ## Development Roadmap 26 | 27 | **Completed Features:** 28 | - Python `BPFObjectBuilder` that takes care of BPF program compilation and loading 29 | - Python `BPFObject` that provides an interface into BPF programs and maps 30 | - The following map types: 31 | - `HASH` 32 | - `PERCPU_HASH` 33 | - `LRU_HASH` 34 | - `LRU_PERCPU_HASH` 35 | - `ARRAY` 36 | - `PERCPU_ARRAY` 37 | - `QUEUE` 38 | - `STACK` 39 | 40 | **Coming Features** 41 | - The following map types: 42 | - `PROG_ARRAY` 43 | - `PERF_EVENT_ARRAY` 44 | - `STACK_TRACE` 45 | - `CGROUP_ARRAY` 46 | - `LPM_TRIE` 47 | - `ARRAY_OF_MAPS` 48 | - `HASH_OF_MAPS` 49 | - `DEVMAP` 50 | - `SOCKMAP` 51 | - `CPUMAP` 52 | - `XSKMAP` 53 | - `SOCKHASH` 54 | - `CGROUP_STORAGE` 55 | - `REUSEPORT_SOCKARRAY` 56 | - `PERCPU_CGROUP_STORAGE` 57 | - `SK_STORAGE` 58 | - `DEVMAP_HASH` 59 | - `STRUCT_OPS` 60 | - USDT, uprobe loading 61 | - `pybpf` CLI tool for bootstrapping PyBPF projects 62 | 63 | **Distant Future:** 64 | - Automatic map key/value type inference 65 | - Automatic per-event type inference 66 | 67 | ## Reference Guide 68 | 69 | Coming soon! 70 | 71 | ## Cool PyBPF Projects 72 | 73 | Coming soon! 74 | -------------------------------------------------------------------------------- /bin/pybpf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 5 | Copyright (C) 2020 William Findlay 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 20 | USA 21 | 22 | 2020-Aug-02 William Findlay Created this. 23 | """ 24 | 25 | from pybpf.cli import main 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /pybpf/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-02 William Findlay Created this. 21 | """ 22 | 23 | from .maps import create_map 24 | from .programs import create_prog 25 | from .lib import Lib 26 | from .bootstrap import Bootstrap 27 | from . import skeleton 28 | from . import programs 29 | from . import maps 30 | 31 | __all__ = ['Bootstrap'] 32 | 33 | try: 34 | from .syscall import syscall_name, syscall_num 35 | __all__ += ['syscall_num', 'syscall_name'] 36 | except Exception: 37 | pass 38 | 39 | -------------------------------------------------------------------------------- /pybpf/bootstrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-29 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import inspect 25 | import subprocess 26 | import logging 27 | import datetime as dt 28 | from dataclasses import dataclass 29 | from typing import Optional, List, Tuple 30 | 31 | from pybpf.skeleton import generate_skeleton 32 | from pybpf.utils import kversion, which, assert_exists, drop_privileges, strip_full_extension, arch, module_path 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | TEMPLATES_DIR = module_path('templates') 37 | 38 | def get_caller_dir(): 39 | try: 40 | cf = inspect.stack()[-1] 41 | except IndexError: 42 | raise Exception("Unable to find caller's directory") 43 | return os.path.dirname(os.path.abspath(cf.filename)) 44 | 45 | class Bootstrap: 46 | VMLINUX_BTF = '/sys/kernel/btf/vmlinux' 47 | 48 | @classmethod 49 | @drop_privileges 50 | def bootstrap(cls, bpf_src: str, outdir: Optional[str] = None) -> Tuple[str, str]: 51 | """ 52 | Combines Bootstrap.generate_vmlinux(), Bootstrap.compile_bpf(), and Bootstrap.generate_skeleton() into one step. 53 | Returns the skeleton class filename and the name of the skeleton class. 54 | """ 55 | assert os.path.isfile(bpf_src) 56 | 57 | bpf_dir = os.path.dirname(bpf_src) 58 | 59 | assert os.path.isdir(bpf_dir) 60 | 61 | vmlinux = cls.generate_vmlinux(bpf_dir) 62 | obj = cls.compile_bpf(bpf_src, outdir=outdir) 63 | skel_file, skel_cls = cls.generate_skeleton(obj, outdir=outdir) 64 | 65 | return skel_file, skel_cls 66 | 67 | @staticmethod 68 | @drop_privileges 69 | def generate_vmlinux(bpfdir: Optional[str] = None, overwrite: bool = False) -> str: 70 | """ 71 | Use bpftool to generate the @bpfdir/vmlinux.h header file symlink for that corresponds with the BTF info for the current kernel. Unless @overwrite is true, existing vmlinux files will not be updated. Only the symbolic link will be updated. 72 | """ 73 | if not bpfdir: 74 | bpfdir = os.path.join(get_caller_dir(), 'bpf') 75 | if not os.path.exists(bpfdir): 76 | raise FileNotFoundError(f'No such directory {bpfdir}') 77 | 78 | bpfdir = os.path.abspath(bpfdir) 79 | 80 | vmlinux_kversion_h = os.path.join(bpfdir, f'vmlinux_{kversion()}.h') 81 | vmlinux_h = os.path.join(bpfdir, 'vmlinux.h') 82 | 83 | if (not os.path.exists(vmlinux_kversion_h)) or overwrite: 84 | try: 85 | bpftool = [which('bpftool')] 86 | except FileNotFoundError: 87 | raise OSError( 88 | 'bpftool not found on system. ' 89 | 'You can install bpftool from linux/tools/bpf/bpftool ' 90 | 'in your kernel sources.' 91 | ) from None 92 | 93 | try: 94 | assert_exists(Bootstrap.VMLINUX_BTF) 95 | except FileNotFoundError: 96 | raise OSError( 97 | f'BTF file {Bootstrap.VMLINUX_BTF} does not exist. ' 98 | 'Please build your kernel with CONFIG_DEBUG_INFO_BTF=y ' 99 | 'or set Bootstrap.VMLINUX_BTF to the correct location.' 100 | ) from None 101 | 102 | bpftool_args = f'btf dump file {Bootstrap.VMLINUX_BTF} format c'.split() 103 | 104 | with open(vmlinux_kversion_h, 'w+') as f: 105 | subprocess.check_call(bpftool + bpftool_args, stdout=f) 106 | 107 | try: 108 | os.unlink(vmlinux_h) 109 | except FileNotFoundError: 110 | pass 111 | os.symlink(vmlinux_kversion_h, vmlinux_h) 112 | 113 | logger.info(f'Generated {vmlinux_h}') 114 | 115 | return vmlinux_h 116 | 117 | @staticmethod 118 | @drop_privileges 119 | def compile_bpf(bpf_src: str, outdir: Optional[str] = None, cflags: List[str] = []) -> str: 120 | """ 121 | Generate the BPF object file for @bpf_src and place it in @outdir. 122 | """ 123 | if not outdir: 124 | outdir = get_caller_dir() 125 | 126 | bpf_src = os.path.abspath(bpf_src) 127 | outdir = os.path.abspath(outdir) 128 | 129 | # Check for source file 130 | try: 131 | assert_exists(bpf_src) 132 | except FileNotFoundError: 133 | raise FileNotFoundError(f'Specified source file {bpf_src} does not exist.') from None 134 | 135 | bpf_dir = os.path.dirname(bpf_src) 136 | 137 | # Check for vmlinux.h 138 | try: 139 | assert_exists(os.path.join(bpf_dir, 'vmlinux.h')) 140 | except FileNotFoundError: 141 | raise FileNotFoundError('Please generate vmlinux.h first with generate_vmlinux().') from None 142 | 143 | obj_file = os.path.join(bpf_dir, strip_full_extension(os.path.basename(bpf_src)) + '.bpf.o') 144 | 145 | # Check for clang 146 | try: 147 | clang = [which('clang')] 148 | except FileNotFoundError: 149 | raise FileNotFoundError('clang not found on system. ' 150 | 'Please install clang and try again.') from None 151 | 152 | clang_args = cflags + f'-g -O2 -target bpf -D__TARGET_ARCH_{arch()}'.split() + f'-c {bpf_src} -o {obj_file}'.split() 153 | 154 | # Check for llvm-strip 155 | try: 156 | llvm_strip = [which('llvm-strip'), '-g', obj_file] 157 | except FileNotFoundError: 158 | raise FileNotFoundError('llvm-strip not found on system. ' 159 | 'Please install llvm-strip and try again.') from None 160 | 161 | # Compile BPF program 162 | logger.info(f'Compiling BPF program {bpf_src} -> {obj_file}') 163 | try: 164 | subprocess.check_call(clang + clang_args, stdout=subprocess.DEVNULL) 165 | except subprocess.CalledProcessError: 166 | raise Exception("Failed to compile BPF program") from None 167 | 168 | # Strip symbols from BPF program 169 | logger.info(f'Stripping symbols from BPF program {obj_file}') 170 | try: 171 | subprocess.check_call(llvm_strip, stdout=subprocess.DEVNULL) 172 | except subprocess.CalledProcessError: 173 | raise Exception("Failed to strip symbols from BPF program") from None 174 | 175 | return obj_file 176 | 177 | @staticmethod 178 | @drop_privileges 179 | def generate_skeleton(bpf_obj_path: str, outdir: Optional[str] = None) -> Tuple[str, str]: 180 | """ 181 | Regenerate the python skeleton file for the @bpf_obj_path. The file will be generated in the same directory as the caller or @outdir if specified. Returns the skeleton class filename and the name of the skeleton class. 182 | """ 183 | if not outdir: 184 | outdir = get_caller_dir() 185 | return generate_skeleton(bpf_obj_path, outdir) 186 | 187 | @dataclass 188 | class ProjectBuilder: 189 | """ 190 | A builder that can be used to create a new pybpf project. 191 | """ 192 | author_name: str 193 | project_name: str 194 | author_email: Optional[str] = None 195 | project_dir: Optional[str] = None 196 | project_git: Optional[str] = None 197 | project_description: Optional[str] = None 198 | overwrite_existing: bool = False 199 | 200 | def __post_init__(self): 201 | now = dt.datetime.now() 202 | self.year, self.month, self.day = now.strftime('%Y %b %d').split() 203 | 204 | def build(self): 205 | """ 206 | Build the pybpf project. 207 | """ 208 | # Apply sensible defaults 209 | if self.author_email is None: 210 | self.author_email = '' 211 | if self.project_dir is None: 212 | self.project_dir = '.' 213 | if self.project_git is None: 214 | self.project_git = '' 215 | if self.project_description is None: 216 | self.project_description = '' 217 | 218 | # Make sure project_dir is an absolute path 219 | self.project_dir = os.path.abspath(self.project_dir) 220 | 221 | # Create project directory 222 | try: 223 | os.makedirs(self.project_dir, exist_ok=False) 224 | except FileExistsError: 225 | if len(os.listdir(self.project_dir)) and not self.overwrite_existing: 226 | raise Exception(f'Refusing to overwrite non-empty project directory {self.project_dir}') from None 227 | 228 | logger.info(f'Creating a new pybpf project in {self.project_dir}...') 229 | 230 | # Copy template files over 231 | for depth, (root, dirs, files) in enumerate(os.walk(TEMPLATES_DIR)): 232 | for _file in files: 233 | tf = os.path.join(root, _file) 234 | 235 | if depth: 236 | subdirs = os.path.join(*root.split(os.sep)[-depth:]) 237 | else: 238 | subdirs = '' 239 | od = os.path.join(self.project_dir, subdirs) 240 | os.makedirs(od, exist_ok=True) 241 | 242 | of = os.path.join(od, _file) 243 | 244 | logger.info(f'Creating {of}...') 245 | 246 | # Read from template file 247 | with open(tf, 'r') as f: 248 | text = f.read() 249 | 250 | # Sub in as appropriate 251 | text = text.replace('PROJECT_NAME', self.project_name) 252 | text = text.replace( 253 | 'PROJECT_DESCRIPTION', self.project_description 254 | ) 255 | text = text.replace('AUTHOR_NAME', self.author_name) 256 | text = text.replace('AUTHOR_EMAIL', self.author_email) 257 | text = text.replace('YEAR', self.year) 258 | text = text.replace('MONTH', self.month) 259 | text = text.replace('DAY', self.day) 260 | 261 | # Write to outfile 262 | with open(of, 'w+') as f: 263 | f.write(text) 264 | 265 | logger.info(f'{self.project_dir} has been bootstrapped successfully!') 266 | -------------------------------------------------------------------------------- /pybpf/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-27 William Findlay Created this. 21 | """ 22 | 23 | from __future__ import annotations 24 | import os 25 | import readline 26 | import logging 27 | from typing import Optional 28 | 29 | import click 30 | 31 | # This is better than nothing. Fixes a readline issue where user can overwrite 32 | # the prompt. https://github.com/pallets/click/issues/665 33 | click.termui.visible_prompt_func = lambda x: input(" ") 34 | 35 | from pybpf.cli.aliased_group import AliasedGroup 36 | from pybpf.cli.pybpf_init import init 37 | from pybpf.cli.pybpf_gen import generate 38 | from pybpf.cli.pybpf_compile import build 39 | 40 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO) 41 | logger = logging.getLogger(__name__) 42 | 43 | @click.group(help='Manage pybpf projects', cls=AliasedGroup) 44 | @click.option('--debug', flag_value=True, default=False, hidden=True) 45 | @click.help_option('-h', '--help') 46 | def pybpf(debug=False): 47 | """ 48 | Main pybpf program. 49 | """ 50 | if debug: 51 | logging.getLogger().setLevel(logging.DEBUG) 52 | 53 | 54 | def main(): 55 | pybpf.add_command(init) 56 | pybpf.add_command(generate) 57 | pybpf.add_command(build) 58 | pybpf() 59 | -------------------------------------------------------------------------------- /pybpf/cli/aliased_group.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | The AliasedGroup class belongs to the click.py contributors and is covered 21 | under the respective license for that project. 22 | 23 | 2020-Aug-27 William Findlay Added this from https://click.palletsprojects.com/en/7.x/advanced/#parameter-modifications. 24 | """ 25 | 26 | import click 27 | 28 | class AliasedGroup(click.Group): 29 | """ 30 | A click group that supports aliases. 31 | Taken from https://click.palletsprojects.com/en/7.x/advanced/#parameter-modifications 32 | """ 33 | def get_command(self, ctx, cmd_name): 34 | rv = click.Group.get_command(self, ctx, cmd_name) 35 | if rv is not None: 36 | return rv 37 | matches = [x for x in self.list_commands(ctx) 38 | if x.startswith(cmd_name)] 39 | if not matches: 40 | return None 41 | elif len(matches) == 1: 42 | return click.Group.get_command(self, ctx, matches[0]) 43 | ctx.fail('Too many matches: %s' % ', '.join(sorted(matches))) 44 | -------------------------------------------------------------------------------- /pybpf/cli/pybpf_compile.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-27 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import logging 25 | 26 | import click 27 | 28 | from pybpf.bootstrap import Bootstrap 29 | from pybpf.cli.aliased_group import AliasedGroup 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | @click.command(help='Build the BPF skeleton object.') 34 | @click.argument('bpf_src', type=click.Path(exists=True, file_okay=True, dir_okay=False), default='./bpf/prog.bpf.c') 35 | @click.help_option('-h', '--help') 36 | def build(bpf_src): 37 | """ 38 | Compile the BPF object for BPF_SRC. 39 | If not specified, BPF_SRC defaults to ./bpf/prog.bpf.c 40 | """ 41 | bpf_src = os.path.abspath(bpf_src) 42 | try: 43 | Bootstrap.compile_bpf(bpf_src) 44 | except Exception as e: 45 | logger.error(f'Unable to compile BPF program: {repr(e)}') 46 | 47 | -------------------------------------------------------------------------------- /pybpf/cli/pybpf_gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-27 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import logging 25 | 26 | import click 27 | 28 | from pybpf.bootstrap import Bootstrap 29 | from pybpf.skeleton import generate_skeleton 30 | from pybpf.cli.aliased_group import AliasedGroup 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | @click.group(cls=AliasedGroup) 36 | @click.help_option('-h', '--help') 37 | def generate(): 38 | """ 39 | Generate files for your pybpf project. 40 | """ 41 | pass 42 | 43 | 44 | @generate.command() 45 | @click.argument('bpf_dir', type=click.Path(exists=True, file_okay=False, dir_okay=True), default='./bpf') 46 | @click.help_option('-h', '--help') 47 | def vmlinux(bpf_dir): 48 | """ 49 | Generate the vmlinux.h header file and place it in BPF_DIR. 50 | If not specified, BPF_DIR defaults to ./bpf 51 | """ 52 | bpf_dir = os.path.abspath(bpf_dir) 53 | try: 54 | Bootstrap.generate_vmlinux(bpfdir=bpf_dir) 55 | except Exception as e: 56 | logger.error(f'Unable to generate vmlinux: {repr(e)}') 57 | 58 | 59 | @generate.command() 60 | @click.argument( 61 | 'bpf', 62 | type=click.Path(dir_okay=False, file_okay=True, exists=True), 63 | ) 64 | @click.argument( 65 | 'outdir', 66 | type=click.Path(dir_okay=True, file_okay=False, exists=True), 67 | default='.' 68 | ) 69 | @click.help_option('-h', '--help') 70 | def skeleton(outdir: str, bpf: str): 71 | """ 72 | Generate the pybpf skeleton file from BPF. 73 | 74 | BPF is the path to the compiled BPF object file. If this file does not yet exist, run "pybpf compile" first. 75 | 76 | OUTDIR is the output directory for the skeleton file. If not specified, defaults to '.' 77 | """ 78 | generate_skeleton(bpf, outdir) 79 | -------------------------------------------------------------------------------- /pybpf/cli/pybpf_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-27 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | from urllib.parse import urlparse 25 | from email.utils import parseaddr as emailparse 26 | 27 | import click 28 | 29 | from pybpf.bootstrap import Bootstrap 30 | 31 | class URL(click.ParamType): 32 | """ 33 | Parse a valid URL. 34 | """ 35 | name = 'url' 36 | 37 | def is_valid(self, url): 38 | try: 39 | result = urlparse(url) 40 | return all([result.scheme, result.netloc, result.path]) 41 | except Exception: 42 | return False 43 | 44 | def convert(self, value, param, ctx): 45 | if not (self.is_valid(value)): 46 | self.fail(f'Invalid URL {value}') 47 | return value 48 | 49 | 50 | class Email(click.ParamType): 51 | """ 52 | Parse a valid email. 53 | """ 54 | name = 'email' 55 | 56 | def convert(self, value, param, ctx): 57 | try: 58 | _name, email = emailparse(value) 59 | except Exception: 60 | _name, email = ('', '') 61 | if not email: 62 | self.fail(f'Invalid email {value}') 63 | if not '@' in email: 64 | self.fail('Email should contain an "@" symbol') 65 | return email 66 | 67 | 68 | def continually_prompt(): 69 | yield True 70 | while True: 71 | if click.confirm( 72 | click.style('Is this correct?', fg='yellow'), default=True 73 | ): 74 | print() 75 | return 76 | yield True 77 | 78 | 79 | @click.command(help='Create a pybpf project') 80 | @click.option( 81 | '--name', 82 | 'author_name', 83 | type=str, 84 | default=None, 85 | help='Name of the project author', 86 | ) 87 | @click.option( 88 | '--email', 89 | 'author_email', 90 | type=Email(), 91 | default=None, 92 | help='Email of the project author', 93 | ) 94 | @click.option( 95 | '--git', 96 | 'project_git', 97 | type=URL(), 98 | default=None, 99 | help='URL for the project git upstream', 100 | ) 101 | @click.option( 102 | '--dir', 103 | 'project_dir', 104 | type=click.Path(file_okay=False, dir_okay=True), 105 | default=None, 106 | help='Directory to create for the project', 107 | ) 108 | @click.option( 109 | '--description', 110 | 'project_description', 111 | type=str, 112 | default=None, 113 | help='A one line description of the project', 114 | ) 115 | @click.option( 116 | '--overwrite', 117 | is_flag=True, 118 | default=False, 119 | help='Overwrite the target directory if it exists', 120 | ) 121 | @click.help_option('-h', '--help') 122 | def init(author_name, author_email, project_git, project_dir, project_description, overwrite): 123 | """ 124 | Initialize a pybpf project. 125 | """ 126 | # Get author name 127 | if author_name is None: 128 | for _ in continually_prompt(): 129 | author_name = click.prompt('Author name') 130 | 131 | # Get author email 132 | if author_email is None: 133 | for _ in continually_prompt(): 134 | author_email = click.prompt( 135 | 'Author email (empty skips)', type=Email(), default='' 136 | ) 137 | 138 | # Get project directory and project name 139 | if project_dir is None: 140 | for _ in continually_prompt(): 141 | project_dir = click.prompt( 142 | 'Project directory', 143 | type=click.Path(file_okay=False, dir_okay=True), 144 | ) 145 | 146 | project_dir = os.path.abspath(project_dir) 147 | project_name = os.path.basename(project_dir) 148 | 149 | # Get project git 150 | if project_git is None: 151 | for _ in continually_prompt(): 152 | project_git = click.prompt( 153 | 'Project git (empty skips)', type=URL(), default='' 154 | ) 155 | 156 | # Get a project description 157 | if project_description is None: 158 | project_description = '' 159 | for _ in continually_prompt(): 160 | project_description = click.prompt( 161 | 'Project description (empty skips)', type=str, default='' 162 | ) 163 | 164 | Bootstrap.ProjectBuilder( 165 | author_name=author_name, 166 | author_email=author_email, 167 | project_name=project_name, 168 | project_dir=project_dir, 169 | project_git=project_git, 170 | project_description=project_description, 171 | overwrite_existing=overwrite 172 | ).build() 173 | 174 | -------------------------------------------------------------------------------- /pybpf/lib.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-02 William Findlay Created this. 21 | """ 22 | 23 | from __future__ import annotations 24 | import os 25 | import ctypes as ct 26 | import subprocess 27 | from typing import get_type_hints, Callable, List, Tuple, Generator 28 | 29 | from pybpf.utils import which, arch, kversion, strip_end 30 | 31 | _LIBBPF = ct.CDLL('libbpf.so', use_errno=True) 32 | 33 | _RINGBUF_CB_TYPE = ct.CFUNCTYPE(ct.c_int, ct.c_void_p, ct.c_void_p, ct.c_int) 34 | 35 | def skeleton_fn(skeleton: ct.CDLL, name: str) -> Callable: 36 | """ 37 | A decorator that wraps a skeleton function of the same name. 38 | """ 39 | def inner(func): 40 | th = get_type_hints(func) 41 | argtypes = [v for k, v in th.items() if k != 'return'] 42 | try: 43 | restype = th['return'] 44 | if restype == type(None): 45 | restype = None 46 | except KeyError: 47 | restype = None 48 | @staticmethod 49 | def wrapper(*args, **kwargs): 50 | return getattr(skeleton, name)(*args, **kwargs) 51 | getattr(skeleton, name).argtypes = argtypes 52 | getattr(skeleton, name).restype = restype 53 | return wrapper 54 | return inner 55 | 56 | def libbpf_fn(name: str) -> Callable: 57 | """ 58 | A decorator that wraps a libbpf function of the same name. 59 | """ 60 | def inner(func): 61 | th = get_type_hints(func) 62 | argtypes = [v for k, v in th.items() if k != 'return'] 63 | try: 64 | restype = th['return'] 65 | if restype == type(None): 66 | restype = None 67 | except KeyError: 68 | restype = None 69 | @staticmethod 70 | def wrapper(*args, **kwargs): 71 | return getattr(_LIBBPF, name)(*args, **kwargs) 72 | getattr(_LIBBPF, name).argtypes = argtypes 73 | getattr(_LIBBPF, name).restype = restype 74 | return wrapper 75 | return inner 76 | 77 | class Lib: 78 | """ 79 | Python bindings for libbpf. 80 | """ 81 | # pylint: disable=no-self-argument,no-method-argument 82 | 83 | # ==================================================================== 84 | # Bookkeeping 85 | # ==================================================================== 86 | 87 | @libbpf_fn('bpf_object__open') 88 | def bpf_object_open(path: ct.c_char_p) -> ct.c_void_p: 89 | pass 90 | 91 | @libbpf_fn('bpf_object__load') 92 | def bpf_object_load(obj: ct.c_void_p) -> ct.c_int: 93 | pass 94 | 95 | @libbpf_fn('bpf_object__close') 96 | def bpf_object_close(obj: ct.c_void_p) -> None: 97 | pass 98 | 99 | # ==================================================================== 100 | # Map Functions 101 | # ==================================================================== 102 | 103 | @libbpf_fn('bpf_object__find_map_fd_by_name') 104 | def find_map_fd_by_name(obj: ct.c_void_p, name: ct.c_char_p) -> ct.c_int: 105 | pass 106 | 107 | @libbpf_fn('bpf_map__fd') 108 | def bpf_map_fd(_map: ct.c_void_p) -> ct.c_int: 109 | pass 110 | 111 | @libbpf_fn('bpf_map__type') 112 | def bpf_map_type(_map: ct.c_void_p) -> ct.c_int: 113 | pass 114 | 115 | @libbpf_fn('bpf_map__key_size') 116 | def bpf_map_key_size(_map: ct.c_void_p) -> ct.c_uint32: 117 | pass 118 | 119 | @libbpf_fn('bpf_map__value_size') 120 | def bpf_map_value_size(_map: ct.c_void_p) -> ct.c_uint32: 121 | pass 122 | 123 | @libbpf_fn('bpf_map__name') 124 | def bpf_map_name(_map: ct.c_void_p) -> ct.c_char_p: 125 | pass 126 | 127 | @libbpf_fn('bpf_map__max_entries') 128 | def bpf_map_max_entries(_map: ct.c_void_p) -> ct.c_uint32: 129 | pass 130 | 131 | @libbpf_fn('bpf_map__next') 132 | def bpf_map_next(_map: ct.c_void_p, obj: ct.c_void_p) -> ct.c_void_p: 133 | pass 134 | 135 | @libbpf_fn('bpf_map__prev') 136 | def bpf_map_prev(_map: ct.c_void_p, obj: ct.c_void_p) -> ct.c_void_p: 137 | pass 138 | 139 | @classmethod 140 | def obj_maps(cls, obj: ct.c_void_p) -> Generator[ct.c_void_p, None, None]: 141 | if not obj: 142 | raise StopIteration('Null BPF object.') 143 | _map = cls.bpf_map_next(None, obj) 144 | while _map: 145 | yield _map 146 | _map = cls.bpf_map_next(_map, obj) 147 | 148 | @libbpf_fn('bpf_map_lookup_elem') 149 | def bpf_map_lookup_elem(map_fd: ct.c_int, key: ct.c_void_p, value: ct.c_void_p) -> ct.c_int: 150 | pass 151 | 152 | @libbpf_fn('bpf_map_update_elem') 153 | def bpf_map_update_elem(map_fd: ct.c_int, key: ct.c_void_p, value: ct.c_void_p, flags :ct.c_int) -> ct.c_int: 154 | pass 155 | 156 | @libbpf_fn('bpf_map_delete_elem') 157 | def bpf_map_delete_elem(map_fd: ct.c_int, key: ct.c_void_p) -> ct.c_int: 158 | pass 159 | 160 | @libbpf_fn('bpf_map_lookup_and_delete_elem') 161 | def bpf_map_lookup_and_delete_elem(map_fd: ct.c_int, key: ct.c_void_p, value: ct.c_void_p) -> ct.c_int: 162 | pass 163 | 164 | @libbpf_fn('bpf_map_get_next_key') 165 | def bpf_map_get_next_key(map_fd: ct.c_int, key: ct.c_void_p, next_key: ct.c_void_p) -> ct.c_int: 166 | pass 167 | 168 | # ==================================================================== 169 | # Libbpf Ringbuf 170 | # ==================================================================== 171 | 172 | @libbpf_fn('ring_buffer__new') 173 | def ring_buffer_new(map_fd: ct.c_int, sample_cb: _RINGBUF_CB_TYPE, ctx: ct.c_void_p, opts: ct.c_void_p) -> ct.c_void_p: 174 | pass 175 | 176 | @libbpf_fn('ring_buffer__free') 177 | def ring_buffer_free(ringbuf: ct.c_void_p) -> None: 178 | pass 179 | 180 | @libbpf_fn('ring_buffer__add') 181 | def ring_buffer_add(ringbuf: ct.c_void_p, map_fd: ct.c_int, sample_cb: _RINGBUF_CB_TYPE, ctx: ct.c_void_p) -> ct.c_int: 182 | pass 183 | 184 | @libbpf_fn('ring_buffer__poll') 185 | def ring_buffer_poll(ringbuf: ct.c_void_p, timeout_ms: ct.c_int) -> ct.c_int: 186 | pass 187 | 188 | @libbpf_fn('ring_buffer__consume') 189 | def ring_buffer_consume(ringbuf: ct.c_void_p) -> ct.c_int: 190 | pass 191 | 192 | # ==================================================================== 193 | # Program Functions 194 | # ==================================================================== 195 | 196 | @libbpf_fn('bpf_program__fd') 197 | def bpf_program_fd(prog: ct.c_void_p) -> ct.c_int: 198 | pass 199 | 200 | @libbpf_fn('bpf_program__get_type') 201 | def bpf_program_type(prog: ct.c_void_p) -> ct.c_int: 202 | pass 203 | 204 | @libbpf_fn('bpf_program__name') 205 | def bpf_program_name(prog: ct.c_void_p) -> ct.c_char_p: 206 | pass 207 | 208 | @libbpf_fn('bpf_program__load') 209 | def bpf_program_load(prog: ct.c_void_p, license: ct.c_char_p, kernel_version: ct.c_uint32) -> ct.c_int: 210 | pass 211 | 212 | @libbpf_fn('bpf_program__attach') 213 | def bpf_program_attach(prog: ct.c_void_p) -> ct.c_void_p: 214 | pass 215 | 216 | @libbpf_fn('bpf_program__next') 217 | def bpf_program_next(prog: ct.c_void_p, obj: ct.c_void_p) -> ct.c_void_p: 218 | pass 219 | 220 | @libbpf_fn('bpf_program__prev') 221 | def bpf_program_prev(prog: ct.c_void_p, obj: ct.c_void_p) -> ct.c_void_p: 222 | pass 223 | 224 | @classmethod 225 | def obj_programs(cls, obj: ct.c_void_p) -> Generator[ct.c_void_p, None, None]: 226 | if not obj: 227 | raise StopIteration('Null BPF object.') 228 | prog = cls.bpf_program_next(None, obj) 229 | while prog: 230 | yield prog 231 | prog = cls.bpf_program_next(prog, obj) 232 | 233 | @libbpf_fn('bpf_prog_test_run') 234 | def bpf_prog_test_run(prog_fd: ct.c_int, repeat: ct.c_int, data: ct.c_void_p, data_size: ct.c_uint32, data_out: ct.c_void_p, data_out_size: ct.POINTER(ct.c_uint32), retval: ct.POINTER(ct.c_uint32), duration: ct.POINTER(ct.c_uint32)) -> ct.c_int: 235 | pass 236 | 237 | @libbpf_fn('bpf_program__attach_xdp') 238 | def bpf_program_attach_xdp(prog: ct.c_void_p, ifindex: ct.c_int) -> ct.c_void_p: 239 | pass 240 | 241 | @libbpf_fn('bpf_set_link_xdp_fd') 242 | def bpf_set_link_xdp_fd(ifindex: ct.c_int, progfd: ct.c_int, flags: ct.c_uint32) -> ct.c_int: 243 | pass 244 | 245 | # ==================================================================== 246 | # Uprobe Attachment 247 | # ==================================================================== 248 | 249 | @libbpf_fn('bpf_program__attach_uprobe') 250 | def attach_uprobe(prog: ct.c_void_p, retprobe: ct.c_bool, pid: ct.c_int, binary_path: ct.c_char_p, func_offset: ct.c_size_t) -> ct.c_void_p: 251 | pass 252 | 253 | # ==================================================================== 254 | # Book Keeping 255 | # ==================================================================== 256 | 257 | @libbpf_fn('libbpf_num_possible_cpus') 258 | def num_possible_cpus() -> ct.c_int: 259 | pass 260 | 261 | # pylint: enable=no-self-argument,no-method-argument 262 | -------------------------------------------------------------------------------- /pybpf/maps.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-08 William Findlay Created this. 21 | """ 22 | 23 | from __future__ import annotations 24 | import ctypes as ct 25 | from struct import pack, unpack 26 | from collections.abc import MutableMapping 27 | from abc import ABC 28 | from enum import IntEnum, auto 29 | from typing import Callable, Any, Optional, Type, Union, TYPE_CHECKING 30 | 31 | from pybpf.lib import Lib, _RINGBUF_CB_TYPE 32 | from pybpf.utils import cerr, force_bytes 33 | 34 | # Maps map type to map class 35 | maptype2class = {} 36 | 37 | def register_map(map_type: BPFMapType): 38 | """ 39 | Decorates a class to register if with the corresponding :IntEnum:BPFMapType. 40 | """ 41 | def inner(_map: Type[MapBase]): 42 | maptype2class[map_type] = _map 43 | return _map 44 | return inner 45 | 46 | class BPFMapType(IntEnum): 47 | """ 48 | Integer enum representing BPF map types. 49 | """ 50 | UNSPEC = 0 51 | HASH = auto() 52 | ARRAY = auto() 53 | PROG_ARRAY = auto() # TODO 54 | PERF_EVENT_ARRAY = auto() # TODO 55 | PERCPU_HASH = auto() 56 | PERCPU_ARRAY = auto() 57 | STACK_TRACE = auto() # TODO 58 | CGROUP_ARRAY = auto() 59 | LRU_HASH = auto() 60 | LRU_PERCPU_HASH = auto() 61 | LPM_TRIE = auto() # TODO 62 | ARRAY_OF_MAPS = auto() # TODO 63 | HASH_OF_MAPS = auto() # TODO 64 | DEVMAP = auto() # TODO 65 | SOCKMAP = auto() # TODO 66 | CPUMAP = auto() # TODO 67 | XSKMAP = auto() # TODO 68 | SOCKHASH = auto() # TODO 69 | CGROUP_STORAGE = auto() 70 | REUSEPORT_SOCKARRAY = auto() # TODO 71 | PERCPU_CGROUP_STORAGE = auto() # TODO 72 | QUEUE = auto() 73 | STACK = auto() 74 | SK_STORAGE = auto() # TODO 75 | DEVMAP_HASH = auto() # TODO 76 | STRUCT_OPS = auto() # TODO 77 | RINGBUF = auto() 78 | # This must be the last entry 79 | MAP_TYPE_UNKNOWN = auto() 80 | 81 | def create_map(skel, _map: ct.c_voidp, map_fd: ct.c_int, mtype: ct.c_int, ksize: ct.c_int, vsize: ct.c_int, max_entries: ct.c_int) -> Union[Type[MapBase], Type[QueueStack], Ringbuf]: 82 | """ 83 | Create a BPF map object from a map description. 84 | """ 85 | # Convert map type to enum 86 | try: 87 | map_type = BPFMapType(mtype) 88 | except ValueError: 89 | map_type = BPFMapType.MAP_TYPE_UNKNOWN 90 | 91 | if map_type == BPFMapType.RINGBUF: 92 | return Ringbuf(skel, _map, map_fd) 93 | 94 | # Construct map based on map type 95 | try: 96 | return maptype2class[map_type](_map, map_fd, ksize, vsize, max_entries) 97 | except KeyError: 98 | pass 99 | 100 | # Fall through 101 | raise ValueError(f'No map implementation for {map_type.name}') 102 | 103 | class MapBase(MutableMapping): 104 | """ 105 | A base class for BPF maps. 106 | """ 107 | def __init__(self, _map: ct.c_void_p, map_fd: int, ksize: int, vsize: int, max_entries: int): 108 | self._map = _map 109 | self._map_fd = map_fd 110 | self._ksize = ksize 111 | self._vsize = vsize 112 | self._max_entries = max_entries 113 | 114 | self.KeyType = self._no_key_type 115 | self.ValueType = self._no_value_type 116 | 117 | def _no_key_type(self, *args, **kwargs): 118 | raise Exception(f'Please define a ctype for key using {self.__class__.__name__}.register_key_type(ctype)') 119 | 120 | def _no_value_type(self, *args, **kwargs): 121 | raise Exception(f'Please define a ctype for value using {self.__class__.__name__}.register_value_type(ctype)') 122 | 123 | def register_key_type(self, _type: ct.Structure): 124 | """ 125 | Register a new ctype as a key type. 126 | """ 127 | if ct.sizeof(_type) != self._ksize: 128 | raise Exception(f'Mismatch between key size ({self._ksize}) and size of key type ({ct.sizeof(_type)})') 129 | self.KeyType = _type 130 | 131 | def register_value_type(self, _type: ct.Structure): 132 | """ 133 | Register a new ctype as a value type. 134 | """ 135 | if ct.sizeof(_type) != self._vsize: 136 | raise Exception(f'Mismatch between value size ({self._vsize}) and size of value type ({ct.sizeof(_type)})') 137 | self.ValueType = _type 138 | 139 | def capacity(self) -> int: 140 | """ 141 | Return the capacity of the map in entries. 142 | """ 143 | return self._max_entries 144 | 145 | def clear(self): 146 | """ 147 | Clear the map, deleting all keys. 148 | """ 149 | for k in self.keys(): 150 | try: 151 | self.__delitem__(k) 152 | except KeyError: 153 | pass 154 | 155 | class Iter: 156 | """ 157 | A helper inner class to iterate through map keys. 158 | This class is taken from https://github.com/iovisor/bcc/blob/master/src/python/bcc/table.py 159 | """ 160 | def __init__(self, _map: MapBase): 161 | self.map = _map 162 | self.key = None 163 | 164 | def __iter__(self): 165 | return self 166 | 167 | def __next__(self): 168 | return self.next() 169 | 170 | def next(self): 171 | self.key = self.map.next(self.key) 172 | return self.key 173 | 174 | def next(self, key): 175 | """ 176 | Returns the next map key. 177 | """ 178 | next_key = self.KeyType() 179 | 180 | if key is None: 181 | ret = Lib.bpf_map_get_next_key(self._map_fd, None, ct.byref(next_key)) 182 | else: 183 | ret = Lib.bpf_map_get_next_key(self._map_fd, ct.byref(key), ct.byref(next_key)) 184 | 185 | if ret < 0: 186 | raise StopIteration() 187 | 188 | return next_key 189 | 190 | def iter(self): 191 | """ 192 | Returns an iterator through map keys. 193 | """ 194 | return self.__iter__() 195 | 196 | def keys(self): 197 | """ 198 | Returns an iterator through map keys. 199 | """ 200 | return self.__iter__() 201 | 202 | def itervalues(self): 203 | """ 204 | Yields map values. 205 | """ 206 | for key in self: 207 | try: 208 | yield self[key] 209 | except KeyError: 210 | pass 211 | 212 | def iteritems(self): 213 | """ 214 | Yields map (key, value) pairs. 215 | """ 216 | for key in self: 217 | try: 218 | yield (key, self[key]) 219 | except KeyError: 220 | pass 221 | 222 | def items(self): 223 | """ 224 | Returns a list of map (key, value) pairs. 225 | """ 226 | return [item for item in self.iteritems()] 227 | 228 | def values(self): 229 | """ 230 | Returns a list of map values. 231 | """ 232 | return [value for value in self.itervalues()] 233 | 234 | def update(self, key: self.KeyType, value: self.ValueType, flags: int): 235 | """ 236 | Update a map value, operating according to specified flags. 237 | This provides more control than the traditional map[key] = value method. 238 | """ 239 | try: 240 | key = self.KeyType(key) 241 | except TypeError: 242 | pass 243 | try: 244 | value = self.ValueType(value) 245 | except TypeError: 246 | pass 247 | ret = Lib.bpf_map_update_elem(self._map_fd, ct.byref(key), ct.byref(value), flags) 248 | if ret < 0: 249 | raise KeyError(f'Unable to update item: {cerr(ret)}') 250 | 251 | def __getitem__(self, key): 252 | value = self.ValueType() 253 | try: 254 | key = self.KeyType(key) 255 | except TypeError: 256 | pass 257 | ret = Lib.bpf_map_lookup_elem(self._map_fd, ct.byref(key), ct.byref(value)) 258 | if ret < 0: 259 | raise KeyError(f'Unable to fetch item: {cerr(ret)}') 260 | return value 261 | 262 | def __setitem__(self, key, value): 263 | self.update(key, value, 0) 264 | 265 | def __delitem__(self, key): 266 | try: 267 | key = self.KeyType(key) 268 | except TypeError: 269 | pass 270 | ret = Lib.bpf_map_delete_elem(self._map_fd, ct.byref(key)) 271 | if ret < 0: 272 | raise KeyError(f'Unable to delete item item: {cerr(ret)}') 273 | 274 | def __iter__(self): 275 | return self.Iter(self) 276 | 277 | def __len__(self): 278 | i = 0 279 | for _k in self: 280 | i += 1 281 | return i 282 | 283 | def __eq__(self, other): 284 | return id(self) == id(other) 285 | 286 | class PerCpuMixin(ABC): 287 | _vsize = None # type: int 288 | def __init__(self, *args, **kwargs): 289 | self._num_cpus = Lib.num_possible_cpus() 290 | try: 291 | alignment = self._vsize % 8 292 | except AttributeError: 293 | raise Exception('PerCpuMixin without value size?!') 294 | # Force aligned value size 295 | if alignment != 0: 296 | self._vsize += (8 - alignment) 297 | # Make sure we are now aligned 298 | alignment = self._vsize % 8 299 | assert alignment == 0 300 | 301 | 302 | def register_value_type(self, _type: ct.Structure): 303 | """ 304 | Register a new ctype as a value type. 305 | Value must be aligned to 8 bytes. 306 | """ 307 | if _type in [ct.c_int, ct.c_int8, ct.c_int16, ct.c_int32, ct.c_byte, ct.c_char]: 308 | _type = ct.c_int64 309 | elif _type in [ct.c_uint, ct.c_uint8, ct.c_uint16, ct.c_uint32, ct.c_ubyte]: 310 | _type = ct.c_uint64 311 | elif ct.sizeof(_type) % 8: 312 | raise ValueError('Value size for percpu maps must be 8-byte aligned') 313 | if ct.sizeof(_type) != self._vsize: 314 | raise Exception(f'Mismatch between value size ({self._vsize}) and size of value type ({ct.sizeof(_type)})') 315 | self.ValueType = _type * self._num_cpus 316 | 317 | @register_map(BPFMapType.HASH) 318 | class Hash(MapBase): 319 | """ 320 | A BPF hashmap. 321 | """ 322 | def __init__(self, *args, **kwargs): 323 | super().__init__(*args, **kwargs) 324 | 325 | @register_map(BPFMapType.LRU_HASH) 326 | class LruHash(Hash): 327 | """ 328 | A BPF hashmap that discards least recently used entries when it is full. 329 | """ 330 | def __init__(self, *args, **kwargs): 331 | super().__init__(*args, **kwargs) 332 | 333 | @register_map(BPFMapType.PERCPU_HASH) 334 | class PerCpuHash(PerCpuMixin, Hash): 335 | """ 336 | A BPF hashmap that maintains unsynchonized copies per cpu. 337 | """ 338 | def __init__(self, *args, **kwargs): 339 | Hash.__init__(self, *args, **kwargs) 340 | PerCpuMixin.__init__(self, *args, **kwargs) 341 | 342 | @register_map(BPFMapType.LRU_PERCPU_HASH) 343 | class LruPerCpuHash(PerCpuMixin, LruHash): 344 | """ 345 | A BPF hashmap that maintains unsynchonized copies per cpu and discards least 346 | recently used entries when it is full. 347 | """ 348 | def __init__(self, *args, **kwargs): 349 | LruHash.__init__(self, *args, **kwargs) 350 | PerCpuMixin.__init__(self, *args, **kwargs) 351 | 352 | @register_map(BPFMapType.ARRAY) 353 | class Array(MapBase): 354 | """ 355 | A BPF array. Keys are always ct.c_uint. 356 | """ 357 | def __init__(self, *args, **kwargs): 358 | super().__init__(*args, **kwargs) 359 | self.KeyType = ct.c_uint 360 | 361 | def register_key_type(self, _type: ct.Structure): 362 | raise NotImplementedError('Arrays always have key type ct.c_uint. This cannot be changed') 363 | 364 | def __delitem__(self, key): 365 | self.__setitem__(key, self.ValueType()) 366 | 367 | @register_map(BPFMapType.CGROUP_ARRAY) 368 | class CgroupArray(Array): 369 | """ 370 | A BPF array that contains cgroup file descriptors. Userspace is expected to 371 | populate this map by getting a cgroup-backed fd by calling open(2) on 372 | a cgroup directory. Then, update the map to contain the cgroup fd. 373 | """ 374 | def __init__(self, *args, **kwargs): 375 | super().__init__(*args, **kwargs) 376 | 377 | self.KeyType = ct.c_uint 378 | self.ValueType = ct.c_uint 379 | 380 | def register_value_type(self, _type: ct.Structure): 381 | raise NotImplementedError('Cgroup always have value type ct.c_uint. This cannot be changed') 382 | 383 | def register_key_type(self, _type: ct.Structure): 384 | raise NotImplementedError('Cgroup always have key type ct.c_uint. This cannot be changed') 385 | 386 | def append_cgroup(self, cgroup_path: str) -> int: 387 | """ 388 | A helper to get the cgroup fd associated with the directory @cgroup_path 389 | and append its file descriptor to the map. Returns the array index that 390 | was updated on success. 391 | """ 392 | with open(cgroup_path, 'r') as f: 393 | fd = f.fileno 394 | if fd < 0: 395 | raise Exception(f'Unable to get file descriptor for cgroup {cgroup_path}') 396 | key = len(self) 397 | self.__setitem__(key, fd) 398 | return key 399 | 400 | @register_map(BPFMapType.PERCPU_ARRAY) 401 | class PerCpuArray(PerCpuMixin, Array): 402 | """ 403 | A BPF array that maintains unsynchonized copies per cpu. Keys are always 404 | ct.c_uint. 405 | """ 406 | def __init__(self, *args, **kwargs): 407 | Array.__init__(self, *args, **kwargs) 408 | PerCpuMixin.__init__(self, *args, **kwargs) 409 | 410 | @register_map(BPFMapType.CGROUP_STORAGE) 411 | class CgroupStorage(MapBase): 412 | """ 413 | A BPF map that maintains per-cgroup value storage. Elements of this map 414 | may not be created or deleted. Instead, they are automatically created 415 | or deleted when a BPF program is attached to a cgroup. 416 | """ 417 | def __init__(self, *args, **kwargs): 418 | super().__init__(*args, **kwargs) 419 | 420 | def register_key_type(self, _type: ct.Structure): 421 | raise NotImplementedError('Cgroup local storage maps always have a specific key type. This cannot be changed') 422 | 423 | def __delitem__(self, key): 424 | raise NotImplementedError('Local storage maps may not delete their elements') 425 | 426 | class QueueStack(ABC): 427 | """ 428 | A base class for BPF stacks and queues. 429 | """ 430 | def __init__(self, _map: ct.c_void_p, map_fd: int, ksize: int, vsize: int, max_entries: int): 431 | self._map = _map 432 | self._map_fd = map_fd 433 | self._ksize = ksize 434 | self._vsize = vsize 435 | self._max_entries = max_entries 436 | 437 | self.ValueType = self._no_value_type 438 | 439 | def _no_value_type(self, *args, **kwargs): 440 | raise Exception(f'Please define a ctype for value using {self.__class__.__name__}.register_value_type(ctype)') 441 | 442 | def register_value_type(self, _type: ct.Structure): 443 | """ 444 | Register a new ctype as a value type. 445 | """ 446 | if ct.sizeof(_type) != self._vsize: 447 | raise Exception(f'Mismatch between value size ({self._vsize}) and size of value type ({ct.sizeof(_type)})') 448 | self.ValueType = _type 449 | 450 | def capacity(self) -> int: 451 | """ 452 | Return the capacity of the map in entries. 453 | """ 454 | return self._max_entries 455 | 456 | def push(self, value: self.ValueType, flags: int = 0): 457 | """ 458 | Push an element onto the map. 459 | """ 460 | try: 461 | value = self.ValueType(value) 462 | except TypeError: 463 | pass 464 | ret = Lib.bpf_map_update_elem(self._map_fd, None, ct.byref(value), flags) 465 | if ret < 0: 466 | raise KeyError(f'Unable to push value: {cerr(ret)}') 467 | 468 | def pop(self) -> self.ValueType: 469 | """ 470 | Pop an element from the map. 471 | """ 472 | value = self.ValueType() 473 | ret = Lib.bpf_map_lookup_and_delete_elem(self._map_fd, None, ct.byref(value)) 474 | if ret < 0: 475 | raise KeyError(f'Unable to pop value: {cerr(ret)}') 476 | return value 477 | 478 | def peek(self): 479 | """ 480 | Peek an element from the map. 481 | """ 482 | value = self.ValueType() 483 | ret = Lib.bpf_map_lookup_elem(self._map_fd, None, ct.byref(value)) 484 | if ret < 0: 485 | raise KeyError(f'Unable to peek value: {cerr(ret)}') 486 | return value 487 | 488 | @register_map(BPFMapType.QUEUE) 489 | class Queue(QueueStack): 490 | """ 491 | A BPF Queue map. Implements a FIFO data structure without a key type. 492 | """ 493 | def __init__(self, *args, **kwargs): 494 | super().__init__(*args, **kwargs) 495 | 496 | @register_map(BPFMapType.STACK) 497 | class Stack(QueueStack): 498 | """ 499 | A BPF Stack map. Implements a LIFO data structure without a key type. 500 | """ 501 | def __init__(self, *args, **kwargs): 502 | super().__init__(*args, **kwargs) 503 | 504 | class Ringbuf: 505 | """ 506 | A ringbuf map for passing per-event data to userspace. This class should not 507 | be instantiated directly. Instead, it is created automatically by the BPFObject. 508 | """ 509 | def __init__(self, skel, _map: ct.c_void_p, map_fd: int): 510 | self._skel = skel 511 | self._map = _map 512 | self.map_fd = map_fd 513 | 514 | if self.map_fd < 0: 515 | raise Exception(f'Bad file descriptor for ringbuf') 516 | 517 | self._cb = None 518 | 519 | def callback(self, data_type: Optional[ct.Structure] = None) -> Callable: 520 | """ 521 | The ringbuf map is the canonical way to pass per-event data to 522 | userspace. This decorator marks a function as a callback for events 523 | from @ringbuf. 524 | 525 | @data_type may be specified to automatically convert the data pointer to 526 | the appropriate ctype. Otherwise, this conversion must be done manually. 527 | 528 | The decorated function must have the following signature: 529 | ``` 530 | def _callback(ctx: Any, data: ct.c_void_p, size: int): 531 | # Do work 532 | ``` 533 | 534 | Optionally, the function may return a non-zero integer to indicate that 535 | polling should be stopped. 536 | """ 537 | # Decorator to register the ringbuf callback 538 | def inner(func): 539 | def wrapper(ctx, data, size): 540 | # Auto convert data type if provided 541 | if data_type is not None: 542 | data = ct.cast(data, ct.POINTER(data_type)).contents 543 | # Call the callback 544 | ret = func(ctx, data, size) 545 | # Callback should always return an int 546 | # If not, fall back to returning zero 547 | try: 548 | ret = int(ret) 549 | except Exception: 550 | ret = 0 551 | return ret 552 | # Open ringbug with provided callback 553 | self._open(self.map_fd, wrapper) 554 | return wrapper 555 | return inner 556 | 557 | def _open(self, map_fd: ct.c_int, func: Callable, ctx: ct.c_void_p = None) -> None: 558 | """ 559 | Open a new ringbuf with @func as a callback. 560 | """ 561 | # Cast func as _RINGBUF_CB_TYPE 562 | func = _RINGBUF_CB_TYPE(func) 563 | # Handle case where we don't have a manager yet 564 | if not self._skel._ringbuf_mgr: 565 | self._skel._ringbuf_mgr = Lib.ring_buffer_new(map_fd, func, ctx, None) 566 | if not self._skel._ringbuf_mgr: 567 | raise Exception(f'Failed to create new ring buffer manager: {cerr()}') 568 | # Handle case where we already have a manager 569 | else: 570 | ret = Lib.ring_buffer_add(self._skel._ringbuf_mgr, map_fd, func, ctx) 571 | if ret != 0: 572 | raise Exception(f'Failed to add ringbuf to ring buffer manager: {cerr(ret)}') 573 | # Keep a refcnt so that our function doesn't get cleaned up 574 | self._cb = func 575 | -------------------------------------------------------------------------------- /pybpf/programs.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-11 William Findlay Created this. 21 | """ 22 | 23 | from __future__ import annotations 24 | import ctypes as ct 25 | from enum import IntEnum, auto 26 | from abc import ABC 27 | from typing import Callable, Any, Optional, Type, TYPE_CHECKING 28 | 29 | from pybpf.lib import Lib, _RINGBUF_CB_TYPE 30 | from pybpf.utils import cerr, force_bytes, get_encoded_kernel_version 31 | 32 | # Maps prog type to prog class 33 | progtype2class = {} 34 | 35 | def register_prog(prog_type: BPFProgType): 36 | """ 37 | Decorates a class to register if with the corresponding :IntEnum:BPFProgType. 38 | """ 39 | def inner(prog: Type[ProgBase]): 40 | progtype2class[prog_type] = prog 41 | return prog 42 | return inner 43 | 44 | class BPFProgType(IntEnum): 45 | """ 46 | Integer enum representing BPF program types. 47 | """ 48 | UNSPEC = 0 49 | SOCKET_FILTER = auto() 50 | KPROBE = auto() 51 | SCHED_CLS = auto() 52 | SCHED_ACT = auto() 53 | TRACEPOINT = auto() 54 | XDP = auto() 55 | PERF_EVENT = auto() 56 | CGROUP_SKB = auto() 57 | CGROUP_SOCK = auto() 58 | LWT_IN = auto() 59 | LWT_OUT = auto() 60 | LWT_XMIT = auto() 61 | SOCK_OPS = auto() 62 | SK_SKB = auto() 63 | CGROUP_DEVICE = auto() 64 | SK_MSG = auto() 65 | RAW_TRACEPOINT = auto() 66 | CGROUP_SOCK_ADDR = auto() 67 | LWT_SEG6LOCAL = auto() 68 | LIRC_MODE2 = auto() 69 | SK_REUSEPORT = auto() 70 | FLOW_DISSECTOR = auto() 71 | CGROUP_SYSCTL = auto() 72 | RAW_TRACEPOINT_WRITABLE = auto() 73 | CGROUP_SOCKOPT = auto() 74 | TRACING = auto() 75 | STRUCT_OPS = auto() 76 | EXT = auto() 77 | LSM = auto() 78 | SK_LOOKUP = auto() 79 | # This must be the last entry 80 | PROG_TYPE_UNKNOWN = auto() 81 | 82 | def create_prog(prog: ct.c_void_p, prog_name: str, prog_type: ct.c_int, prog_fd: ct.c_int) -> Optional[ProgBase]: 83 | """ 84 | Create a BPF prog object from a prog description. 85 | """ 86 | # Convert prog type to enum 87 | try: 88 | prog_type = BPFProgType(prog_type) 89 | except ValueError: 90 | prog_type = BPFProgType.PROG_TYPE_UNKNOWN 91 | 92 | # Construct prog based on prog type 93 | try: 94 | return progtype2class[prog_type](prog, prog_name, prog_fd) 95 | except KeyError: 96 | pass 97 | 98 | # Fall through 99 | raise ValueError(f'No prog implementation for {prog_type.name}') 100 | 101 | class ProgBase(ABC): 102 | """ 103 | A base class for BPF programs. 104 | """ 105 | def __init__(self, prog: ct.c_void_p, name: str, prog_fd: int): 106 | if not prog: 107 | raise Exception(f'Null program pointer for {name}') 108 | self._prog = prog 109 | self._name = name 110 | self._prog_fd = prog_fd 111 | self._link = None # type: ct.c_void_p 112 | 113 | def __eq__(self, other): 114 | return id(self) == id(other) 115 | 116 | def attach(self): 117 | """ 118 | Attach the BPF program. 119 | """ 120 | if self._link: 121 | return 122 | self._link = Lib.bpf_program_attach(self._prog) 123 | if not self._link: 124 | raise Exception(f'Failed to attach BPF program {self._name}: {cerr()}') 125 | 126 | def invoke(self, data: ct.Structure = None): 127 | """ 128 | Invoke the BPF program once and capture and return its return value. 129 | If the program is of type BPF_PROG_TYPE_USER, you can pass it data 130 | using a ctypes struct @data. 131 | """ 132 | if data == None: 133 | data_p = None 134 | data_size = ct.c_uint32(0) 135 | else: 136 | data_p = ct.addressof(data) 137 | data_size = ct.sizeof(data) 138 | 139 | # Make this signed so that we can express negative return values, 140 | # should be okay to pass to the unsigned retval pointer 141 | bpf_ret = ct.c_uint32() 142 | 143 | retval = Lib.bpf_prog_test_run(self._prog_fd, ct.c_int(1), data_p, data_size, None, ct.c_uint32(0), ct.byref(bpf_ret), None) 144 | if retval < 0: 145 | raise Exception(f'Failed to invoke BPF program {self._name}: {cerr(retval)}') 146 | 147 | bpf_retval = ct.c_int16(bpf_ret.value) 148 | return bpf_retval.value 149 | 150 | @register_prog(BPFProgType.SOCKET_FILTER) 151 | class ProgSocketFilter(ProgBase): 152 | def __init__(self, *args, **kwargs): 153 | super().__init__(*args, **kwargs) 154 | 155 | @register_prog(BPFProgType.KPROBE) 156 | class ProgKprobe(ProgBase): 157 | def __init__(self, *args, **kwargs): 158 | super().__init__(*args, **kwargs) 159 | 160 | def invoke(self, data: ct.Structure = None): 161 | raise NotImplementedError(f'{self.__class__.__name__} programs cannot yet be invoked with bpf_prog_test_run.') 162 | 163 | @register_prog(BPFProgType.SCHED_CLS) 164 | class ProgSchedCls(ProgBase): 165 | def __init__(self, *args, **kwargs): 166 | super().__init__(*args, **kwargs) 167 | 168 | @register_prog(BPFProgType.SCHED_ACT) 169 | class ProgSchedAct(ProgBase): 170 | def __init__(self, *args, **kwargs): 171 | super().__init__(*args, **kwargs) 172 | 173 | @register_prog(BPFProgType.TRACEPOINT) 174 | class ProgTracepoint(ProgBase): 175 | def __init__(self, *args, **kwargs): 176 | super().__init__(*args, **kwargs) 177 | 178 | def invoke(self, data: ct.Structure = None): 179 | raise NotImplementedError(f'{self.__class__.__name__} programs cannot yet be invoked with bpf_prog_test_run.') 180 | 181 | @register_prog(BPFProgType.XDP) 182 | class ProgXdp(ProgBase): 183 | def __init__(self, *args, **kwargs): 184 | super().__init__(*args, **kwargs) 185 | 186 | def attach_xdp(self, *ifnames: str): 187 | """ 188 | Attach the XDP program to interfaces with names @ifnames. 189 | """ 190 | try: 191 | from pyroute2 import IPRoute 192 | except ImportError: 193 | raise ImportError('pyroute2 must be installed before attaching an XDP program') from None 194 | ipr = IPRoute() 195 | for ifname in ifnames: 196 | try: 197 | ifindex = ipr.link_lookup(ifname=ifname)[0] 198 | except IndexError: 199 | raise KeyError(f'No such interface "{ifname}"') from None 200 | retval = Lib.bpf_set_link_xdp_fd(ifindex, self._prog_fd, 0) 201 | if retval < 0: 202 | raise Exception(f'Failed to attach XDP program {self._name} to interface "{ifname}" ({ifindex}): {cerr(retval)}') 203 | 204 | def remove_xdp(self, *ifnames: str): 205 | """ 206 | Remove all XDP programs from interfaces with names @ifnames. 207 | """ 208 | try: 209 | from pyroute2 import IPRoute 210 | except ImportError: 211 | raise ImportError('pyroute2 must be installed before removing XDP programs') from None 212 | ipr = IPRoute() 213 | for ifname in ifnames: 214 | try: 215 | ifindex = ipr.link_lookup(ifname=ifname)[0] 216 | except IndexError: 217 | raise KeyError(f'No such interface "{ifname}"') from None 218 | retval = Lib.bpf_set_link_xdp_fd(ifindex, -1, 0) 219 | if retval < 0: 220 | raise Exception(f'Failed to remove XDP program {self._name} to interface "{ifname}" ({ifindex}): {cerr(retval)}') 221 | 222 | @register_prog(BPFProgType.PERF_EVENT) 223 | class ProgPerfEvent(ProgBase): 224 | def __init__(self, *args, **kwargs): 225 | super().__init__(*args, **kwargs) 226 | 227 | @register_prog(BPFProgType.CGROUP_SKB) 228 | class ProgCgroupSkb(ProgBase): 229 | def __init__(self, *args, **kwargs): 230 | super().__init__(*args, **kwargs) 231 | 232 | @register_prog(BPFProgType.CGROUP_SOCK) 233 | class ProgCgroupSock(ProgBase): 234 | def __init__(self, *args, **kwargs): 235 | super().__init__(*args, **kwargs) 236 | 237 | @register_prog(BPFProgType.LWT_IN) 238 | class ProgLwtIn(ProgBase): 239 | def __init__(self, *args, **kwargs): 240 | super().__init__(*args, **kwargs) 241 | 242 | @register_prog(BPFProgType.LWT_OUT) 243 | class ProgLwtOut(ProgBase): 244 | def __init__(self, *args, **kwargs): 245 | super().__init__(*args, **kwargs) 246 | 247 | @register_prog(BPFProgType.LWT_XMIT) 248 | class ProgLwtXmit(ProgBase): 249 | def __init__(self, *args, **kwargs): 250 | super().__init__(*args, **kwargs) 251 | 252 | @register_prog(BPFProgType.SOCK_OPS) 253 | class ProgSockOps(ProgBase): 254 | def __init__(self, *args, **kwargs): 255 | super().__init__(*args, **kwargs) 256 | 257 | @register_prog(BPFProgType.SK_SKB) 258 | class ProgSkSkb(ProgBase): 259 | def __init__(self, *args, **kwargs): 260 | super().__init__(*args, **kwargs) 261 | 262 | @register_prog(BPFProgType.CGROUP_DEVICE) 263 | class ProgCgroupDevice(ProgBase): 264 | def __init__(self, *args, **kwargs): 265 | super().__init__(*args, **kwargs) 266 | 267 | @register_prog(BPFProgType.SK_MSG) 268 | class ProgSkMsg(ProgBase): 269 | def __init__(self, *args, **kwargs): 270 | super().__init__(*args, **kwargs) 271 | 272 | @register_prog(BPFProgType.RAW_TRACEPOINT) 273 | class ProgRawTracepoint(ProgBase): 274 | def __init__(self, *args, **kwargs): 275 | super().__init__(*args, **kwargs) 276 | 277 | @register_prog(BPFProgType.CGROUP_SOCK_ADDR) 278 | class ProgCgroupSockAddr(ProgBase): 279 | def __init__(self, *args, **kwargs): 280 | super().__init__(*args, **kwargs) 281 | 282 | @register_prog(BPFProgType.LWT_SEG6LOCAL) 283 | class ProgLwtSeg6local(ProgBase): 284 | def __init__(self, *args, **kwargs): 285 | super().__init__(*args, **kwargs) 286 | 287 | @register_prog(BPFProgType.LIRC_MODE2) 288 | class ProgLircMode2(ProgBase): 289 | def __init__(self, *args, **kwargs): 290 | super().__init__(*args, **kwargs) 291 | 292 | @register_prog(BPFProgType.SK_REUSEPORT) 293 | class ProgSkReuseport(ProgBase): 294 | def __init__(self, *args, **kwargs): 295 | super().__init__(*args, **kwargs) 296 | 297 | @register_prog(BPFProgType.FLOW_DISSECTOR) 298 | class ProgFlowDissector(ProgBase): 299 | def __init__(self, *args, **kwargs): 300 | super().__init__(*args, **kwargs) 301 | 302 | @register_prog(BPFProgType.CGROUP_SYSCTL) 303 | class ProgCgroupSysctl(ProgBase): 304 | def __init__(self, *args, **kwargs): 305 | super().__init__(*args, **kwargs) 306 | 307 | @register_prog(BPFProgType.RAW_TRACEPOINT_WRITABLE) 308 | class ProgRawTracepointWritable(ProgBase): 309 | def __init__(self, *args, **kwargs): 310 | super().__init__(*args, **kwargs) 311 | 312 | @register_prog(BPFProgType.CGROUP_SOCKOPT) 313 | class ProgCgroupSockopt(ProgBase): 314 | def __init__(self, *args, **kwargs): 315 | super().__init__(*args, **kwargs) 316 | 317 | @register_prog(BPFProgType.TRACING) 318 | class ProgTracing(ProgBase): 319 | def __init__(self, *args, **kwargs): 320 | super().__init__(*args, **kwargs) 321 | 322 | @register_prog(BPFProgType.STRUCT_OPS) 323 | class ProgStructOps(ProgBase): 324 | def __init__(self, *args, **kwargs): 325 | super().__init__(*args, **kwargs) 326 | 327 | @register_prog(BPFProgType.EXT) 328 | class ProgExt(ProgBase): 329 | def __init__(self, *args, **kwargs): 330 | super().__init__(*args, **kwargs) 331 | 332 | @register_prog(BPFProgType.LSM) 333 | class ProgLsm(ProgBase): 334 | def __init__(self, *args, **kwargs): 335 | super().__init__(*args, **kwargs) 336 | 337 | @register_prog(BPFProgType.SK_LOOKUP) 338 | class ProgSkLookup(ProgBase): 339 | def __init__(self, *args, **kwargs): 340 | super().__init__(*args, **kwargs) 341 | -------------------------------------------------------------------------------- /pybpf/skeleton.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-27 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import logging 25 | import ctypes as ct 26 | from textwrap import dedent 27 | 28 | from pybpf.utils import drop_privileges, strip_full_extension, to_camel, force_bytes, cerr, FILESYSTEMENCODING 29 | from pybpf.programs import create_prog 30 | from pybpf.maps import create_map 31 | from pybpf.lib import Lib 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | def open_bpf_object(bpf_obj_path: str) -> ct.c_void_p: 36 | bpf_obj_path = force_bytes(bpf_obj_path) 37 | res = Lib.bpf_object_open(bpf_obj_path) 38 | if not res: 39 | raise Exception(f'Failed to open BPF object: {cerr()}') 40 | return res 41 | 42 | def load_bpf_object(bpf_obj: ct.c_void_p): 43 | res = Lib.bpf_object_load(bpf_obj) 44 | if res < 0: 45 | raise Exception(f'Failed to load BPF object: {cerr()}') 46 | return res 47 | 48 | def close_bpf_object(bpf_obj: ct.c_void_p): 49 | if not bpf_obj: 50 | return 51 | Lib.bpf_object_close(bpf_obj) 52 | 53 | def generate_progs(bpf_obj: ct.c_void_p): 54 | progs = {} 55 | for prog in Lib.obj_programs(bpf_obj): 56 | if not prog: 57 | continue 58 | prog_fd = Lib.bpf_program_fd(prog) 59 | prog_name = Lib.bpf_program_name(prog).decode(FILESYSTEMENCODING) 60 | prog_type = Lib.bpf_program_type(prog) 61 | progs[prog_name] = create_prog(prog, prog_name, prog_type, prog_fd) 62 | return progs 63 | 64 | def generate_maps(skel, bpf_obj: ct.c_void_p): 65 | maps = {} 66 | for _map in Lib.obj_maps(bpf_obj): 67 | if not _map: 68 | continue 69 | map_fd = Lib.bpf_map_fd(_map) 70 | map_name = Lib.bpf_map_name(_map).decode(FILESYSTEMENCODING) 71 | map_entries = Lib.bpf_map_max_entries(_map) 72 | map_ksize = Lib.bpf_map_key_size(_map) 73 | map_vsize = Lib.bpf_map_value_size(_map) 74 | map_type = Lib.bpf_map_type(_map) 75 | maps[map_name] = create_map(skel, _map, map_fd, map_type, map_ksize, map_vsize, map_entries) 76 | return maps 77 | 78 | def generate_skeleton_class(bpf_obj_path: str, bpf_obj_name: str, bpf_class_name: str): 79 | SKEL_CLASS = f""" 80 | from __future__ import annotations 81 | from collections.abc import Mapping 82 | import os 83 | import resource 84 | import atexit 85 | from typing import Callable, Type, TypeVar, NamedTuple, Union 86 | 87 | from pybpf import Lib 88 | from pybpf.skeleton import generate_maps, generate_progs, open_bpf_object, close_bpf_object 89 | from pybpf.maps import MapBase, QueueStack, Ringbuf 90 | from pybpf.programs import ProgBase 91 | 92 | __all__ = ['{bpf_class_name}Skeleton'] 93 | 94 | BPF_OBJECT = '{bpf_obj_path}' 95 | 96 | class ImmutableDict(Mapping): 97 | def __init__(self, _dict): 98 | self._dict = dict(_dict) 99 | self._hash = None 100 | 101 | def __getattr__(self, key): 102 | return self.__getitem__(key) 103 | 104 | def __getitem__(self, key): 105 | return self._dict[key] 106 | 107 | def __len__(self): 108 | return len(self._dict) 109 | 110 | def __iter__(self): 111 | return iter(self._dict) 112 | 113 | def __hash__(self): 114 | if self._hash is None: 115 | self._hash = hash(frozenset(self._dict.items())) 116 | return self._hash 117 | 118 | def __eq__(self, other): 119 | return self._dict == other._dict 120 | 121 | def __str__(self): 122 | return f'ImmutableDict({{self._dict}})' 123 | 124 | class ProgDict(ImmutableDict): 125 | def __getattr__(self, key) -> Type[ProgBase]: 126 | return self.__getitem__(key) 127 | 128 | def __getitem__(self, key) -> Type[MapBase]: 129 | return self._dict[key] 130 | 131 | class MapDict(ImmutableDict): 132 | def __getattr__(self, key) -> Union[Type[MapBase], Type[QueueStack], Ringbuf]: 133 | return self.__getitem__(key) 134 | 135 | def __getitem__(self, key) -> Union[Type[MapBase], Type[QueueStack], Ringbuf]: 136 | return self._dict[key] 137 | 138 | class {bpf_class_name}Skeleton: 139 | \"\"\" 140 | {bpf_class_name}Skeleton is a skeleton class that provides helper methods for accessing the BPF object {bpf_obj_name}. 141 | \"\"\" 142 | 143 | def _initialization_function(self): 144 | pass 145 | 146 | def __init__(self, autoload: bool = True, bump_rlimit: bool = True): 147 | self._ringbuf_mgr = None 148 | 149 | if os.geteuid() != 0: 150 | raise OSError('Using eBPF requries root privileges') 151 | 152 | self.progs = ProgDict({{}}) 153 | self.maps = MapDict({{}}) 154 | 155 | if bump_rlimit: 156 | self._bump_rlimit() 157 | 158 | if autoload: 159 | self._autoload() 160 | 161 | def _bump_rlimit(self): 162 | try: 163 | resource.setrlimit(resource.RLIMIT_MEMLOCK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) 164 | except Exception as e: 165 | raise OSError(f'Failed to bump memory rlimit: {{repr(e)}}') from None 166 | 167 | def _autoload(self): 168 | self.open_bpf() 169 | self._initialization_function() 170 | self.load_bpf() 171 | self.attach_bpf() 172 | 173 | def _cleanup(self): 174 | if self.bpf_object: 175 | close_bpf_object(self.bpf_object) 176 | if self._ringbuf_mgr: 177 | Lib.ring_buffer_free(self._ringbuf_mgr) 178 | 179 | @classmethod 180 | def register_init_fn(cls, fn: Callable[{bpf_class_name}Skeleton, None]) -> None: 181 | \"\"\" 182 | Register an initialization function @fn before instantiating the skeleton class. @fn should take one parameter, the skeleton object, and can then operate on the skeleton object, for example by initializing map values before the BPF programs are loaded. The initialization function is ONLY called when the skeleton is set to auto load, _after_ the BPF object has been opened but _before_ the BPF programs are loaded and attached. 183 | \"\"\" 184 | cls._initialization_function = fn 185 | 186 | def open_bpf(self): 187 | \"\"\" 188 | Open the BPF object managed by this skeleton. 189 | \"\"\" 190 | self.bpf_object = open_bpf_object(BPF_OBJECT) 191 | 192 | def load_bpf(self): 193 | \"\"\" 194 | Load the BPF programs managed by this skeleton. 195 | \"\"\" 196 | res = Lib.bpf_object_load(self.bpf_object) 197 | if res < 0: 198 | raise Exception('Unable to load BPF object') 199 | self.progs = ProgDict(generate_progs(self.bpf_object)) 200 | self.maps = MapDict(generate_maps(self, self.bpf_object)) 201 | atexit.register(self._cleanup) 202 | 203 | def attach_bpf(self): 204 | \"\"\" 205 | Attach the BPF programs managed by this skeleton. 206 | \"\"\" 207 | for prog in self.progs.values(): 208 | prog.attach() 209 | 210 | def ringbuf_consume(self): 211 | \"\"\" 212 | Consume all open ringbuf buffers, regardless of whether or not they currently contain event data. As this method avoids making calls to epoll_wait, it is best for use cases where low latency is desired, but it can impact performance. If you are unsure, use ring_buffer_poll instead. 213 | \"\"\" 214 | if not self._ringbuf_mgr: 215 | raise Exception('No ring buffers to consume. ' 216 | 'Register ring buffers using @skel.maps.ringbuf.callback()') 217 | return Lib.ring_buffer_consume(self._ringbuf_mgr) 218 | 219 | def ringbuf_poll(self, timeout: int = -1): 220 | \"\"\" 221 | Poll for events from all open ring buffers, calling the provided callback for each ringbuffer. @timeout specifies a polling timeout in ms. By default, polling continues indefinitely. 222 | \"\"\" 223 | if not self._ringbuf_mgr: 224 | raise Exception('No ring buffers to poll. ' 225 | 'Register ring buffers using @skel.maps.ringbuf.callback()') 226 | return Lib.ring_buffer_poll(self._ringbuf_mgr, timeout) 227 | """ 228 | 229 | return SKEL_CLASS 230 | 231 | 232 | @drop_privileges 233 | def generate_skeleton(bpf_obj_path: str, outdir: str) -> str: 234 | SKEL_PREAMBLE = """ 235 | \"\"\" 236 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 237 | Copyright (C) 2020 William Findlay 238 | 239 | This library is free software; you can redistribute it and/or 240 | modify it under the terms of the GNU Lesser General Public 241 | License as published by the Free Software Foundation; either 242 | version 2.1 of the License, or (at your option) any later version. 243 | 244 | This library is distributed in the hope that it will be useful, 245 | but WITHOUT ANY WARRANTY; without even the implied warranty of 246 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 247 | Lesser General Public License for more details. 248 | 249 | You should have received a copy of the GNU Lesser General Public 250 | License along with this library; if not, write to the Free Software 251 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 252 | USA 253 | 254 | WARNING: THIS FILE IS AUTOGENERATED BY PYBPF. 255 | DO NOT MAKE MODIFICATIONS TO IT. 256 | 257 | To regenerate this file, run: "pybpf generate skeleton /path/to/prog.bpf.o" 258 | \"\"\" 259 | """ 260 | 261 | SKEL_FILENAME = '{}_skel.py' 262 | 263 | bpf_obj_name = strip_full_extension(os.path.basename(bpf_obj_path)) 264 | bpf_class_name = to_camel(bpf_obj_name, True) 265 | 266 | 267 | txt = SKEL_PREAMBLE 268 | txt += generate_skeleton_class(bpf_obj_path, bpf_obj_name, bpf_class_name) 269 | 270 | # Determine output path 271 | outfile = SKEL_FILENAME.format(bpf_obj_name) 272 | outpath = os.path.abspath(os.path.join(outdir, outfile)) 273 | 274 | with open(outpath, 'w+') as f: 275 | f.write(dedent(txt.strip('\n'))) 276 | logger.debug('\n' + dedent(txt.strip('\n'))) 277 | 278 | return outpath, f'{bpf_class_name}Skeleton' 279 | -------------------------------------------------------------------------------- /pybpf/syscall.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | Portions of this file are taken from https://github.com/iovisor/bcc/blob/master/src/python/bcc/syscall.py 21 | Credit and copyright goes to original authors (Sasha Goldshtein et al). 22 | Original bcc code was licensed under the Apache license. 23 | 24 | 2020-Aug-02 William Findlay Created this. 25 | """ 26 | 27 | import subprocess 28 | 29 | from pybpf.utils import which, FILESYSTEMENCODING 30 | 31 | try: 32 | which('ausyscall') 33 | except FileNotFoundError: 34 | raise Exception('pybpf.syscalls requires the ausyscall program. Please install ausyscall.') from None 35 | 36 | def _parse_syscall(line): 37 | parts = line.split() 38 | return (int(parts[0]), parts[1].strip().decode(FILESYSTEMENCODING)) 39 | 40 | # Taken from https://github.com/iovisor/bcc/blob/master/src/python/bcc/syscall.py 41 | out = subprocess.check_output(['ausyscall', '--dump'], stderr=subprocess.DEVNULL) 42 | out = out.split(b'\n',1)[1] 43 | syscalls = dict(map(_parse_syscall, out.strip().split(b'\n'))) 44 | syscalls_rev = {v: k for k, v in syscalls.items()} 45 | 46 | def syscall_name(syscall_num: int) -> str: 47 | """ 48 | Convert a system call number to name. 49 | """ 50 | return syscalls.get(syscall_num, '[unknown: {syscall_num}]') 51 | 52 | def syscall_num(syscall_name: str) -> int: 53 | """ 54 | Convert a system call name to number. 55 | """ 56 | return syscalls_rev.get(syscall_name, -1) 57 | -------------------------------------------------------------------------------- /pybpf/templates/bpf/prog.bpf.c: -------------------------------------------------------------------------------- 1 | /* 2 | * PROJECT_NAME 3 | * Copyright (C) YEAR AUTHOR_NAME 4 | * 5 | * PROJECT_DESCRIPTION 6 | * 7 | * 8 | * 9 | * YEAR-MONTH-DAY AUTHOR_NAME Created this. 10 | */ 11 | 12 | #include "prog.bpf.h" 13 | 14 | // TODO: Your maps here 15 | 16 | // TODO: Your programs here 17 | -------------------------------------------------------------------------------- /pybpf/templates/bpf/prog.bpf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * PROJECT_NAME 3 | * Copyright (C) YEAR AUTHOR_NAME 4 | * 5 | * PROJECT_DESCRIPTION 6 | * 7 | * 8 | * 9 | * YEAR-MONTH-DAY AUTHOR_NAME Created this. 10 | */ 11 | 12 | #ifndef PROG_BPF_H 13 | #define PROG_BPF_H 14 | 15 | #include "pybpf.bpf.h" 16 | 17 | // TODO: Your definitions here 18 | 19 | #endif /* ifndef PROG_BPF_H */ 20 | -------------------------------------------------------------------------------- /pybpf/templates/bpf/pybpf.bpf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | * Copyright (C) 2020 William Findlay 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2.1 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | * USA 19 | * 20 | * 2020-Aug-08 William Findlay Created this. */ 21 | 22 | #ifndef PYBPF_AUTO_INCLUDES_H 23 | #define PYBPF_AUTO_INCLUDES_H 24 | 25 | #include "vmlinux.h" 26 | 27 | #include /* for BPF CO-RE helpers */ 28 | #include /* most used helpers: SEC, __always_inline, etc */ 29 | #include /* for getting kprobe arguments */ 30 | 31 | #ifndef PAGE_SIZE 32 | #define PAGE_SIZE 4096 33 | #endif 34 | 35 | #ifndef NULL 36 | #define NULL 0 37 | #endif 38 | 39 | /* ========================================================================= 40 | * General-Purpose Helpers 41 | * ========================================================================= */ 42 | 43 | #define lock_xadd(ptr, val) ((void)__sync_fetch_and_add(ptr, val)) 44 | 45 | static __always_inline void *bpf_map_lookup_or_try_init(void *map, const void * key, const void *val) { 46 | void *res = bpf_map_lookup_elem(map, key); 47 | if (!res) { 48 | bpf_map_update_elem(map, key, val, BPF_NOEXIST); 49 | } 50 | return bpf_map_lookup_elem(map, key); 51 | } 52 | 53 | /* ========================================================================= 54 | * Map Definition Helpers 55 | * ========================================================================= */ 56 | 57 | /* Declare a BPF ringbuf map @NAME with 2^(@PAGES) size */ 58 | #define BPF_RINGBUF(NAME, PAGES) \ 59 | struct { \ 60 | __uint(type, BPF_MAP_TYPE_RINGBUF); \ 61 | __uint(max_entries, ((1 << PAGES) * PAGE_SIZE)); \ 62 | } NAME SEC(".maps") 63 | 64 | /* Declare a BPF hashmap @NAME with key type @KEY, value type @VALUE, and @SIZE 65 | * max entries. The map creation flags may be specified with @FLAGS. */ 66 | #define BPF_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 67 | struct { \ 68 | __uint(type, BPF_MAP_TYPE_HASH); \ 69 | __uint(max_entries, SIZE); \ 70 | __type(key, KEY); \ 71 | __type(value, VALUE); \ 72 | __uint(map_flags, FLAGS); \ 73 | } NAME SEC(".maps") 74 | 75 | /* Declare an LRU BPF hashmap @NAME with key type @KEY, value type @VALUE, and 76 | * @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 77 | #define BPF_LRU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 78 | struct { \ 79 | __uint(type, BPF_MAP_TYPE_LRU_HASH); \ 80 | __uint(max_entries, SIZE); \ 81 | __type(key, KEY); \ 82 | __type(value, VALUE); \ 83 | __uint(map_flags, FLAGS); \ 84 | } NAME SEC(".maps") 85 | 86 | /* Declare a per-cpu BPF hashmap @NAME with key type @KEY, value type @VALUE, and 87 | * @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 88 | #define BPF_PERCPU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 89 | struct { \ 90 | __uint(type, BPF_MAP_TYPE_PERCPU_HASH); \ 91 | __uint(max_entries, SIZE); \ 92 | __type(key, KEY); \ 93 | __type(value, VALUE); \ 94 | __uint(map_flags, FLAGS); \ 95 | } NAME SEC(".maps") 96 | 97 | /* Declare a per-cpu LRU BPF hashmap @NAME with key type @KEY, value type @VALUE, 98 | * and @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 99 | #define BPF_LRU_PERCPU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 100 | struct { \ 101 | __uint(type, BPF_MAP_TYPE_LRU_PERCPU_HASH); \ 102 | __uint(max_entries, SIZE); \ 103 | __type(key, KEY); \ 104 | __type(value, VALUE); \ 105 | __uint(map_flags, FLAGS); \ 106 | } NAME SEC(".maps") 107 | 108 | /* Declare a BPF array @NAME with value type @VALUE, and @SIZE max entries. 109 | * The map creation flags may be specified with @FLAGS. */ 110 | #define BPF_ARRAY(NAME, VALUE, SIZE, FLAGS) \ 111 | struct { \ 112 | __uint(type, BPF_MAP_TYPE_ARRAY); \ 113 | __uint(max_entries, SIZE); \ 114 | __type(key, unsigned int); \ 115 | __type(value, VALUE); \ 116 | __uint(map_flags, FLAGS); \ 117 | } NAME SEC(".maps") 118 | 119 | /* Declare a per-cpu BPF array @NAME with value type @VALUE, and @SIZE max 120 | * entries. The map creation flags may be specified with @FLAGS. */ 121 | #define BPF_PERCPU_ARRAY(NAME, VALUE, SIZE, FLAGS) \ 122 | struct { \ 123 | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); \ 124 | __uint(max_entries, SIZE); \ 125 | __type(key, unsigned int); \ 126 | __type(value, VALUE); \ 127 | __uint(map_flags, FLAGS); \ 128 | } NAME SEC(".maps") 129 | 130 | /* Declare a cgroup storage map @NAME with value type @VALUE. 131 | * The map creation flags may be specified with @FLAGS. */ 132 | #define BPF_CGROUP_STORAGE(NAME, VALUE, FLAGS) \ 133 | struct { \ 134 | __uint(type, BPF_MAP_TYPE_CGROUP_STORAGE); \ 135 | __uint(max_entries, 0); \ 136 | __type(key, struct bpf_cgroup_storage_key); \ 137 | __type(value, VALUE); \ 138 | __uint(map_flags, FLAGS); \ 139 | } NAME SEC(".maps") 140 | 141 | /* Declare a cgroup fd array @NAME with @SIZE max entries. The map creation 142 | * flags may be specified with @FLAGS. To populate the map, userspace should 143 | * obtain a cgroup-backed fd by calling open(2) on a cgroup directory. This fd 144 | * can then be inserted into the map from userspace. */ 145 | #define BPF_CGROUP_ARRAY(NAME, SIZE, FLAGS) \ 146 | struct { \ 147 | __uint(type, BPF_MAP_TYPE_CGROUP_ARRAY); \ 148 | __uint(max_entries, SIZE); \ 149 | __uint(key_size, sizeof(unsigned int)); \ 150 | __uint(value_size, sizeof(unsigned int)); \ 151 | __uint(map_flags, FLAGS); \ 152 | } NAME SEC(".maps") 153 | 154 | /* Declare a BPF stack @NAME with value type @VALUE, and @SIZE max entries. 155 | * The map creation flags may be specified with @FLAGS. */ 156 | #define BPF_STACK(NAME, VALUE, SIZE, FLAGS) \ 157 | struct { \ 158 | __uint(type, BPF_MAP_TYPE_STACK); \ 159 | __uint(max_entries, SIZE); \ 160 | __uint(key_size, 0); \ 161 | __uint(value_size, sizeof(VALUE)); \ 162 | __uint(map_flags, FLAGS); \ 163 | } NAME SEC(".maps") 164 | 165 | /* Declare a BPF queue @NAME with value type @VALUE, and @SIZE max entries. 166 | * The map creation flags may be specified with @FLAGS. */ 167 | #define BPF_QUEUE(NAME, VALUE, SIZE, FLAGS) \ 168 | struct { \ 169 | __uint(type, BPF_MAP_TYPE_QUEUE); \ 170 | __uint(max_entries, SIZE); \ 171 | __uint(key_size, 0); \ 172 | __uint(value_size, sizeof(VALUE)); \ 173 | __uint(map_flags, FLAGS); \ 174 | } NAME SEC(".maps") 175 | 176 | /* TODO: add remaining map types */ 177 | 178 | #endif /* ifndef PYBPF_AUTO_INCLUDES_H */ 179 | -------------------------------------------------------------------------------- /pybpf/templates/bpf_program.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | PROJECT_NAME 5 | Copyright (C) YEAR AUTHOR_NAME 6 | 7 | PROJECT_DESCRIPTION 8 | 9 | 10 | 11 | YEAR-MONTH-DAY AUTHOR_NAME Created this. 12 | """ 13 | 14 | import os 15 | import time 16 | 17 | from pybpf import Bootstrap 18 | 19 | PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) 20 | BPF_DIR = os.path.join(PROJECT_DIR, 'bpf') 21 | BPF_SRC = os.path.join(BPF_DIR, 'prog.bpf.c') 22 | 23 | # For development only: 24 | Bootstrap.bootstrap(BPF_SRC) 25 | 26 | from prog_skel import ProgSkeleton 27 | 28 | TICKSLEEP = 0.1 29 | 30 | def main(): 31 | 32 | skel = ProgSkeleton() 33 | 34 | # TODO: Your code here 35 | 36 | while 1: 37 | time.sleep(0.1) 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /pybpf/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-02 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import sys 25 | import re 26 | import subprocess 27 | import platform 28 | from enum import IntEnum, auto 29 | from ctypes import get_errno 30 | from typing import Union, Optional 31 | 32 | 33 | def module_path(pathname: str) -> str: 34 | """ 35 | Get path to a file from the root directory of this module. 36 | """ 37 | return os.path.realpath(os.path.join(os.path.dirname(__file__), pathname)) 38 | 39 | 40 | def project_path(pathname: str) -> str: 41 | """ 42 | Get path to a file from the root directory of this project. 43 | """ 44 | return os.path.realpath( 45 | os.path.join(os.path.dirname(__file__), '..', pathname) 46 | ) 47 | 48 | 49 | class LibbpfErrno(IntEnum): 50 | LIBELF = 4000 # Libelf error 51 | FORMAT = auto() # BPF object format invalid 52 | KVERSION = auto() # Incorrect or no 'version' section 53 | ENDIAN = auto() # Endian mismatch 54 | INTERNAL = auto() # Internal error in libbpf 55 | RELOC = auto() # Relocation failed 56 | LOAD = auto() # Load program failure for unknown reason 57 | VERIFY = auto() # Kernel verifier blocks program loading 58 | PROG2BIG = auto() # Program too big 59 | KVER = auto() # Incorrect kernel version 60 | PROGTYPE = auto() # Kernel doesn't support this program type 61 | WRNGPID = auto() # Wrong pid in netlink message 62 | INVSEQ = auto() # Invalid netlink sequence 63 | NLPARSE = auto() # Netlink parsing error 64 | 65 | 66 | libbpf_errstr = { 67 | LibbpfErrno.LIBELF: 'Libelf error', 68 | LibbpfErrno.FORMAT: 'Invalid BPF object format', 69 | LibbpfErrno.KVERSION: 'Incorrect or missing version section', 70 | LibbpfErrno.ENDIAN: 'Endianness mismatch', 71 | LibbpfErrno.INTERNAL: 'Internal libbpf error', 72 | LibbpfErrno.RELOC: 'Relocation failed', 73 | LibbpfErrno.LOAD: 'Unknown load failure', 74 | LibbpfErrno.VERIFY: 'Blocked by verifier', 75 | LibbpfErrno.PROG2BIG: 'BPF program too large', 76 | LibbpfErrno.KVER: 'Incorrect kernel version', 77 | LibbpfErrno.PROGTYPE: 'Kernel does not support this program type', 78 | LibbpfErrno.WRNGPID: 'Wrong PID in netlink message', 79 | LibbpfErrno.INVSEQ: 'Invalid netlink sequence', 80 | LibbpfErrno.NLPARSE: 'Netlink parsing error', 81 | } 82 | 83 | 84 | def cerr(errno: int = None): 85 | """ 86 | Get errno from ctypes and print it. 87 | """ 88 | if errno is None: 89 | errno = get_errno() 90 | if errno < 0: 91 | errno = -int(errno) 92 | try: 93 | errstr = libbpf_errstr[LibbpfErrno(errno)] 94 | except (ValueError, KeyError): 95 | errstr = os.strerror(errno) 96 | return f'{errstr} ({errno})' 97 | 98 | 99 | def which(program: str) -> Union[str, None]: 100 | """ 101 | Find an executable in $PATH and return its abolute path 102 | if it exists. 103 | """ 104 | 105 | def is_exe(fpath): 106 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 107 | 108 | fpath, _fname = os.path.split(program) 109 | if fpath: 110 | if is_exe(program): 111 | return program 112 | else: 113 | for path in os.environ["PATH"].split(os.pathsep): 114 | exe_file = os.path.join(path, program) 115 | if is_exe(exe_file): 116 | return exe_file 117 | 118 | raise FileNotFoundError(f'Unable to find executable for {program}') 119 | 120 | 121 | def assert_exists(f: str) -> None: 122 | """ 123 | Raise an OSError if file @f does not exist. 124 | """ 125 | if f is None or not os.path.exists(f): 126 | raise FileNotFoundError(f'Unable to find {f}') 127 | 128 | 129 | def strip_end(text, suffix): 130 | """ 131 | Strip the end off of a string. 132 | """ 133 | if not text.endswith(suffix): 134 | return text 135 | return text[: len(text) - len(suffix)] 136 | 137 | 138 | extension_re = re.compile(r'([^\.]*)\..*') 139 | 140 | 141 | def strip_full_extension(pathname: str): 142 | """ 143 | Strip an entire extension from a pathname. 144 | """ 145 | head, tail = os.path.split(pathname) 146 | match = extension_re.fullmatch(tail) 147 | if match: 148 | tail = match[1] 149 | return os.path.join(head, tail) 150 | 151 | 152 | def arch() -> str: 153 | """ 154 | Get the current system architecture. 155 | """ 156 | uname = [which('uname'), '-m'] 157 | arch = subprocess.check_output(uname).decode('utf-8').strip() 158 | arch = arch.replace('x86_64', 'x86') 159 | return arch 160 | 161 | 162 | def kversion() -> str: 163 | """ 164 | Get the current system kernel version. 165 | """ 166 | uname = [which('uname'), '-r'] 167 | version = subprocess.check_output(uname).decode('utf-8').strip() 168 | return version 169 | 170 | 171 | FILESYSTEMENCODING = sys.getfilesystemencoding() 172 | 173 | 174 | def force_bytes(s: Union[str, bytes]): 175 | """ 176 | Convert @s to bytes if it is not already bytes. 177 | """ 178 | if isinstance(s, str): 179 | s = s.encode(FILESYSTEMENCODING) 180 | if not isinstance(s, bytes): 181 | raise Exception(f'{s} could not be converted to bytes.') 182 | return s 183 | 184 | 185 | def drop_privileges(function): 186 | """ 187 | Decorator to drop root privileges. 188 | """ 189 | 190 | def inner(*args, **kwargs): 191 | # If not root, just call the function 192 | if os.geteuid() != 0: 193 | return function(*args, **kwargs) 194 | # Get sudoer's UID 195 | try: 196 | sudo_uid = int(os.environ['SUDO_UID']) 197 | except (KeyError, ValueError): 198 | # print("Could not get UID for sudoer", file=sys.stderr) 199 | # TODO log 200 | return 201 | # Get sudoer's GID 202 | try: 203 | sudo_gid = int(os.environ['SUDO_GID']) 204 | except (KeyError, ValueError): 205 | # print("Could not get GID for sudoer", file=sys.stderr) 206 | # TODO log 207 | return 208 | # Make sure groups are reset 209 | try: 210 | os.setgroups([]) 211 | except PermissionError: 212 | # TODO log 213 | pass 214 | # Drop root 215 | os.setresgid(sudo_gid, sudo_gid, -1) 216 | os.setresuid(sudo_uid, sudo_uid, -1) 217 | # Execute function 218 | ret = function(*args, **kwargs) 219 | # Get root back 220 | os.setresgid(0, 0, -1) 221 | os.setresuid(0, 0, -1) 222 | return ret 223 | 224 | return inner 225 | 226 | 227 | def find_file_up_tree(fname: str, starting_dir: str = '.') -> Optional[str]: 228 | """ 229 | Visit parent directories until we find file @fname. 230 | """ 231 | starting_dir = os.path.abspath(starting_dir) 232 | 233 | if not os.path.exists(starting_dir): 234 | raise FileNotFoundError( 235 | f'Starting directory {starting_dir} does not exist' 236 | ) from None 237 | if not os.path.isdir(starting_dir): 238 | raise NotADirectoryError(f'Path {starting_dir} is not a directory') 239 | 240 | head = starting_dir 241 | 242 | while 1: 243 | if fname in os.listdir(head): 244 | return os.path.abspath(os.path.join(head, fname)) 245 | if not head or head == '/': 246 | break 247 | head, _tail = os.path.split(head) 248 | 249 | return None 250 | 251 | def _capitalize(s: str) -> str: 252 | if not s: 253 | return s 254 | if len(s) == 1: 255 | return s.upper() 256 | return s[0].upper() + s[1:] 257 | 258 | def to_camel(s: str, upper: bool) -> str: 259 | """ 260 | Convert a string to camel case. 261 | """ 262 | components = re.split(r'[-_.,]', s) 263 | components = list(map(lambda c: _capitalize(c.strip()), components)) 264 | if not upper: 265 | components[0] = components[0].lower() 266 | return ''.join(components) 267 | 268 | version_re = re.compile(r'(\d*)\.(\d*)\.(\d*)\.') 269 | kversion_cache = None 270 | def get_encoded_kernel_version() -> int: 271 | global kversion_cache 272 | if kversion_cache is not None: 273 | return kversion_cache 274 | version = platform.release() 275 | match = version_re.match(version) 276 | if not match: 277 | return 0 278 | major = int(match[1]) 279 | minor = int(match[2]) 280 | patch = int(match[3]) 281 | # /usr/include/linux/version.h 282 | kversion_cache = ((major) << 16) + ((minor) << 8) + patch 283 | return kversion_cache 284 | -------------------------------------------------------------------------------- /pybpf/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-02 William Findlay Created this. 21 | """ 22 | 23 | import pkg_resources 24 | 25 | try: 26 | __version__ = pkg_resources.require('pybpf')[0].version 27 | except Exception: 28 | __version__ = 'unknown' 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | click==7.1.2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 5 | Copyright (C) 2020 William Findlay 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 20 | USA 21 | 22 | 2020-Aug-02 William Findlay Created this. 23 | """ 24 | 25 | import os, sys 26 | import re 27 | from distutils.core import setup, Extension 28 | from distutils.command.build_ext import build_ext 29 | 30 | NAME = 'pybpf' 31 | VERSION = '0.0.1' 32 | 33 | setup( 34 | name=NAME, 35 | version=VERSION, 36 | description='A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3', 37 | author='William Findlay', 38 | author_email='william@williamfindlay.com', 39 | url='https://github.com/willfindlay/pybpf', 40 | packages=['pybpf'], 41 | scripts=['bin/pybpf'], 42 | include_package_data=True, 43 | package_data={'': ['pybpf/libbpf']}, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | !lib 2 | -------------------------------------------------------------------------------- /tests/bpf_src/.gitignore: -------------------------------------------------------------------------------- 1 | vmlinux* 2 | *.bpf.o 3 | -------------------------------------------------------------------------------- /tests/bpf_src/cgroup.bpf.c: -------------------------------------------------------------------------------- 1 | #include "pybpf.bpf.h" 2 | 3 | BPF_CGROUP_ARRAY(cgroup_fds, 10240, 0); 4 | BPF_CGROUP_STORAGE(cgroup_storage, u64, 0); 5 | -------------------------------------------------------------------------------- /tests/bpf_src/hello.bpf.c: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | #include "vmlinux.h" 3 | // clang-format on 4 | 5 | #include /* for BPF CO-RE helpers */ 6 | #include /* most used helpers: SEC, __always_inline, etc */ 7 | #include /* for getting kprobe arguments */ 8 | 9 | SEC("tracepoint/raw_syscalls/sys_enter") 10 | int sys_enter(struct trace_event_raw_sys_enter *args) 11 | { 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /tests/bpf_src/maps.bpf.c: -------------------------------------------------------------------------------- 1 | #include "pybpf.bpf.h" /* Auto generated helpers */ 2 | 3 | /* A ringbuf */ 4 | BPF_RINGBUF(ringbuf, 1); 5 | 6 | /* hash map types */ 7 | BPF_HASH(hash, int, int, 10240, 0); 8 | BPF_LRU_HASH(lru_hash, int, int, 10240, 0); 9 | BPF_PERCPU_HASH(percpu_hash, int, int, 10240, 0); 10 | BPF_LRU_PERCPU_HASH(lru_percpu_hash, int, int, 10240, 0); 11 | 12 | /* array types */ 13 | BPF_ARRAY(array, int, 10240, 0); 14 | BPF_PERCPU_ARRAY(percpu_array, int, 10240, 0); 15 | 16 | /* stack and queue */ 17 | BPF_STACK(stack, int, 10240, 0); 18 | BPF_QUEUE(queue, int, 10240, 0); 19 | -------------------------------------------------------------------------------- /tests/bpf_src/prog.bpf.c: -------------------------------------------------------------------------------- 1 | #include "pybpf.bpf.h" 2 | 3 | SEC("fentry/bpf_modify_return_test") 4 | int fentry_modify_return_test(void *args) 5 | { 6 | return 0; 7 | } 8 | 9 | SEC("fexit/bpf_modify_return_test") 10 | int fexit_modify_return_test(void *args) 11 | { 12 | return 0; 13 | } 14 | 15 | SEC("tracepoint/raw_syscalls/sys_enter") 16 | int tracepoint_sys_enter(void *args) 17 | { 18 | return 0; 19 | } 20 | 21 | SEC("tp_btf/sys_enter") 22 | int BPF_PROG(tp_btf_sys_enter, struct pt_regs *regs, long id) 23 | { 24 | return 0; 25 | } 26 | 27 | char _license[] SEC("license") = "GPL"; 28 | -------------------------------------------------------------------------------- /tests/bpf_src/pybpf.bpf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | * Copyright (C) 2020 William Findlay 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2.1 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | * USA 19 | * 20 | * 2020-Aug-08 William Findlay Created this. */ 21 | 22 | #ifndef PYBPF_AUTO_INCLUDES_H 23 | #define PYBPF_AUTO_INCLUDES_H 24 | 25 | #include "vmlinux.h" 26 | 27 | #include /* for BPF CO-RE helpers */ 28 | #include /* most used helpers: SEC, __always_inline, etc */ 29 | #include /* for getting kprobe arguments */ 30 | 31 | #ifndef PAGE_SIZE 32 | #define PAGE_SIZE 4096 33 | #endif 34 | 35 | #ifndef NULL 36 | #define NULL 0 37 | #endif 38 | 39 | /* ========================================================================= 40 | * General-Purpose Helpers 41 | * ========================================================================= */ 42 | 43 | #define lock_xadd(ptr, val) ((void)__sync_fetch_and_add(ptr, val)) 44 | 45 | static __always_inline void *bpf_map_lookup_or_try_init(void *map, const void * key, const void *val) { 46 | void *res = bpf_map_lookup_elem(map, key); 47 | if (!res) { 48 | bpf_map_update_elem(map, key, val, BPF_NOEXIST); 49 | } 50 | return bpf_map_lookup_elem(map, key); 51 | } 52 | 53 | /* ========================================================================= 54 | * Map Definition Helpers 55 | * ========================================================================= */ 56 | 57 | /* Declare a BPF ringbuf map @NAME with 2^(@PAGES) size */ 58 | #define BPF_RINGBUF(NAME, PAGES) \ 59 | struct { \ 60 | __uint(type, BPF_MAP_TYPE_RINGBUF); \ 61 | __uint(max_entries, ((1 << PAGES) * PAGE_SIZE)); \ 62 | } NAME SEC(".maps") 63 | 64 | /* Declare a BPF hashmap @NAME with key type @KEY, value type @VALUE, and @SIZE 65 | * max entries. The map creation flags may be specified with @FLAGS. */ 66 | #define BPF_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 67 | struct { \ 68 | __uint(type, BPF_MAP_TYPE_HASH); \ 69 | __uint(max_entries, SIZE); \ 70 | __type(key, KEY); \ 71 | __type(value, VALUE); \ 72 | __uint(map_flags, FLAGS); \ 73 | } NAME SEC(".maps") 74 | 75 | /* Declare an LRU BPF hashmap @NAME with key type @KEY, value type @VALUE, and 76 | * @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 77 | #define BPF_LRU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 78 | struct { \ 79 | __uint(type, BPF_MAP_TYPE_LRU_HASH); \ 80 | __uint(max_entries, SIZE); \ 81 | __type(key, KEY); \ 82 | __type(value, VALUE); \ 83 | __uint(map_flags, FLAGS); \ 84 | } NAME SEC(".maps") 85 | 86 | /* Declare a per-cpu BPF hashmap @NAME with key type @KEY, value type @VALUE, and 87 | * @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 88 | #define BPF_PERCPU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 89 | struct { \ 90 | __uint(type, BPF_MAP_TYPE_PERCPU_HASH); \ 91 | __uint(max_entries, SIZE); \ 92 | __type(key, KEY); \ 93 | __type(value, VALUE); \ 94 | __uint(map_flags, FLAGS); \ 95 | } NAME SEC(".maps") 96 | 97 | /* Declare a per-cpu LRU BPF hashmap @NAME with key type @KEY, value type @VALUE, 98 | * and @SIZE max entries. The map creation flags may be specified with @FLAGS. */ 99 | #define BPF_LRU_PERCPU_HASH(NAME, KEY, VALUE, SIZE, FLAGS) \ 100 | struct { \ 101 | __uint(type, BPF_MAP_TYPE_LRU_PERCPU_HASH); \ 102 | __uint(max_entries, SIZE); \ 103 | __type(key, KEY); \ 104 | __type(value, VALUE); \ 105 | __uint(map_flags, FLAGS); \ 106 | } NAME SEC(".maps") 107 | 108 | /* Declare a BPF array @NAME with value type @VALUE, and @SIZE max entries. 109 | * The map creation flags may be specified with @FLAGS. */ 110 | #define BPF_ARRAY(NAME, VALUE, SIZE, FLAGS) \ 111 | struct { \ 112 | __uint(type, BPF_MAP_TYPE_ARRAY); \ 113 | __uint(max_entries, SIZE); \ 114 | __type(key, unsigned int); \ 115 | __type(value, VALUE); \ 116 | __uint(map_flags, FLAGS); \ 117 | } NAME SEC(".maps") 118 | 119 | /* Declare a per-cpu BPF array @NAME with value type @VALUE, and @SIZE max 120 | * entries. The map creation flags may be specified with @FLAGS. */ 121 | #define BPF_PERCPU_ARRAY(NAME, VALUE, SIZE, FLAGS) \ 122 | struct { \ 123 | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); \ 124 | __uint(max_entries, SIZE); \ 125 | __type(key, unsigned int); \ 126 | __type(value, VALUE); \ 127 | __uint(map_flags, FLAGS); \ 128 | } NAME SEC(".maps") 129 | 130 | /* Declare a cgroup storage map @NAME with value type @VALUE. 131 | * The map creation flags may be specified with @FLAGS. */ 132 | #define BPF_CGROUP_STORAGE(NAME, VALUE, FLAGS) \ 133 | struct { \ 134 | __uint(type, BPF_MAP_TYPE_CGROUP_STORAGE); \ 135 | __uint(max_entries, 0); \ 136 | __type(key, struct bpf_cgroup_storage_key); \ 137 | __type(value, VALUE); \ 138 | __uint(map_flags, FLAGS); \ 139 | } NAME SEC(".maps") 140 | 141 | /* Declare a cgroup fd array @NAME with @SIZE max entries. The map creation 142 | * flags may be specified with @FLAGS. To populate the map, userspace should 143 | * obtain a cgroup-backed fd by calling open(2) on a cgroup directory. This fd 144 | * can then be inserted into the map from userspace. */ 145 | #define BPF_CGROUP_ARRAY(NAME, SIZE, FLAGS) \ 146 | struct { \ 147 | __uint(type, BPF_MAP_TYPE_CGROUP_ARRAY); \ 148 | __uint(max_entries, SIZE); \ 149 | __uint(key_size, sizeof(unsigned int)); \ 150 | __uint(value_size, sizeof(unsigned int)); \ 151 | __uint(map_flags, FLAGS); \ 152 | } NAME SEC(".maps") 153 | 154 | /* Declare a BPF stack @NAME with value type @VALUE, and @SIZE max entries. 155 | * The map creation flags may be specified with @FLAGS. */ 156 | #define BPF_STACK(NAME, VALUE, SIZE, FLAGS) \ 157 | struct { \ 158 | __uint(type, BPF_MAP_TYPE_STACK); \ 159 | __uint(max_entries, SIZE); \ 160 | __uint(key_size, 0); \ 161 | __uint(value_size, sizeof(VALUE)); \ 162 | __uint(map_flags, FLAGS); \ 163 | } NAME SEC(".maps") 164 | 165 | /* Declare a BPF queue @NAME with value type @VALUE, and @SIZE max entries. 166 | * The map creation flags may be specified with @FLAGS. */ 167 | #define BPF_QUEUE(NAME, VALUE, SIZE, FLAGS) \ 168 | struct { \ 169 | __uint(type, BPF_MAP_TYPE_QUEUE); \ 170 | __uint(max_entries, SIZE); \ 171 | __uint(key_size, 0); \ 172 | __uint(value_size, sizeof(VALUE)); \ 173 | __uint(map_flags, FLAGS); \ 174 | } NAME SEC(".maps") 175 | 176 | /* TODO: add remaining map types */ 177 | 178 | #endif /* ifndef PYBPF_AUTO_INCLUDES_H */ 179 | -------------------------------------------------------------------------------- /tests/bpf_src/ringbuf.bpf.c: -------------------------------------------------------------------------------- 1 | #include "pybpf.bpf.h" /* Auto generated helpers */ 2 | 3 | BPF_RINGBUF(ringbuf, 3); 4 | BPF_RINGBUF(ringbuf2, 3); 5 | 6 | SEC("tracepoint/syscalls/sys_enter_nanosleep") 7 | int do_nanosleep(struct trace_event_raw_sys_enter *args) 8 | { 9 | int *five = bpf_ringbuf_reserve(&ringbuf, sizeof(int), 0); 10 | if (five) { 11 | *five = 5; 12 | bpf_ringbuf_submit(five, BPF_RB_FORCE_WAKEUP); 13 | } 14 | 15 | int *ten = bpf_ringbuf_reserve(&ringbuf2, sizeof(int), 0); 16 | if (ten) { 17 | *ten = 10; 18 | bpf_ringbuf_submit(ten, BPF_RB_FORCE_WAKEUP); 19 | } 20 | return 0; 21 | } 22 | 23 | SEC("tracepoint/syscalls/sys_enter_clock_nanosleep") 24 | int do_clock_nanosleep(struct trace_event_raw_sys_enter *args) 25 | { 26 | int *five = bpf_ringbuf_reserve(&ringbuf, sizeof(int), 0); 27 | if (five) { 28 | *five = 5; 29 | bpf_ringbuf_submit(five, BPF_RB_FORCE_WAKEUP); 30 | } 31 | 32 | int *ten = bpf_ringbuf_reserve(&ringbuf2, sizeof(int), 0); 33 | if (ten) { 34 | *ten = 10; 35 | bpf_ringbuf_submit(ten, BPF_RB_FORCE_WAKEUP); 36 | } 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /tests/bpf_src/xdp.bpf.c: -------------------------------------------------------------------------------- 1 | #include "pybpf.bpf.h" 2 | 3 | BPF_ARRAY(packet_count, int, 1, 0); 4 | 5 | SEC("xdp") 6 | int xdp_prog(struct xdp_md *ctx) { 7 | int zero = 0; 8 | int *count = bpf_map_lookup_elem(&packet_count, &zero); 9 | if (!count) 10 | return XDP_PASS; 11 | lock_xadd(count, 1); 12 | return XDP_PASS; 13 | } 14 | 15 | char _license[] SEC("license") = "GPL"; 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-05 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import sys 25 | import shutil 26 | 27 | import pytest 28 | 29 | from pybpf.bootstrap import Bootstrap 30 | from pybpf.skeleton import generate_skeleton 31 | from pybpf.utils import drop_privileges 32 | 33 | TESTDIR = '/tmp/pybpf' 34 | 35 | @drop_privileges 36 | def make_testdir(): 37 | os.makedirs(TESTDIR) 38 | 39 | @pytest.fixture 40 | def testdir(): 41 | try: 42 | shutil.rmtree(TESTDIR) 43 | except FileNotFoundError: 44 | pass 45 | make_testdir() 46 | yield TESTDIR 47 | 48 | @pytest.fixture 49 | def skeleton(testdir): 50 | import importlib.util 51 | def _do_skeleton(bpf_src: str, *args, **kwargs): 52 | skel_file, skel_cls = Bootstrap.bootstrap(bpf_src=bpf_src, outdir=testdir) 53 | d, f = os.path.split(skel_file) 54 | spec = importlib.util.spec_from_file_location(f'{skel_cls}', skel_file) 55 | skel_mod = importlib.util.module_from_spec(spec) 56 | spec.loader.exec_module(skel_mod) 57 | return getattr(skel_mod, skel_cls)(*args, **kwargs) 58 | yield _do_skeleton 59 | -------------------------------------------------------------------------------- /tests/test_maps.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-08 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import time 25 | import subprocess 26 | import ctypes as ct 27 | from multiprocessing import cpu_count 28 | 29 | import pytest 30 | 31 | from pybpf.maps import create_map 32 | from pybpf.utils import project_path, which 33 | 34 | BPF_SRC = project_path('tests/bpf_src') 35 | 36 | def test_ringbuf(skeleton): 37 | """ 38 | Test that ringbuf maps can pass data to userspace from BPF programs. 39 | """ 40 | try: 41 | which('sleep') 42 | except FileNotFoundError: 43 | pytest.skip('sleep not found on system') 44 | 45 | skel = skeleton(os.path.join(BPF_SRC, 'ringbuf.bpf.c')) 46 | 47 | res = 0 48 | res2 = 0 49 | 50 | @skel.maps.ringbuf.callback(ct.c_int) 51 | def _callback(ctx, data, size): 52 | nonlocal res 53 | res = data.value 54 | 55 | @skel.maps.ringbuf2.callback(ct.c_int) 56 | def _callback(ctx, data, size): 57 | nonlocal res2 58 | res2 = data.value 59 | 60 | subprocess.check_call('sleep 0.1'.split()) 61 | skel.ringbuf_consume() 62 | 63 | assert res == 5 64 | assert res2 == 10 65 | 66 | res = 0 67 | res2 = 0 68 | 69 | subprocess.check_call('sleep 0.1'.split()) 70 | skel.ringbuf_poll(10) 71 | 72 | assert res == 5 73 | assert res2 == 10 74 | 75 | def test_bad_ringbuf(skeleton): 76 | """ 77 | Test that attempting to register a callback for a non-existent ringbuf 78 | raises a KeyError. 79 | """ 80 | skel = skeleton(os.path.join(BPF_SRC, 'ringbuf.bpf.c')) 81 | 82 | with pytest.raises(KeyError): 83 | @skel.maps.ringbuf3.callback(ct.c_int) 84 | def _callback(ctx, data, size): 85 | print('unreachable!') 86 | 87 | def test_maps_smoke(skeleton): 88 | """ 89 | Make sure maps load properly. 90 | """ 91 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 92 | 93 | EXPECTED_MAP_COUNT = 9 94 | 95 | if len(skel.maps) > EXPECTED_MAP_COUNT: 96 | pytest.xfail(f'EXPECTED_MAP_COUNT should be updated to {len(skel.maps)}') 97 | 98 | assert len(skel.maps) == EXPECTED_MAP_COUNT 99 | 100 | def test_bad_map(skeleton): 101 | """ 102 | Test that accessing a non-existent map raises a KeyError. 103 | """ 104 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 105 | 106 | with pytest.raises(KeyError): 107 | skel.maps.foo 108 | 109 | with pytest.raises(KeyError): 110 | skel.maps['foo'] 111 | 112 | def test_hash(skeleton): 113 | """ 114 | Test BPF_HASH. 115 | """ 116 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 117 | 118 | # Register key and value type 119 | skel.maps.hash.register_key_type(ct.c_int) 120 | skel.maps.hash.register_value_type(ct.c_int) 121 | 122 | _hash = skel.maps.hash 123 | 124 | assert len(_hash) == 0 125 | 126 | # Try to query the empty map 127 | for i in range(_hash.capacity()): 128 | with pytest.raises(KeyError): 129 | _hash[i] 130 | 131 | # Fill the map 132 | for i in range(_hash.capacity()): 133 | _hash[i] = i 134 | assert len(_hash) == _hash.capacity() 135 | 136 | # Try to add to full map 137 | with pytest.raises(KeyError): 138 | _hash[_hash.capacity()] = 666 139 | 140 | # Query the full map 141 | for i in range(_hash.capacity()): 142 | assert _hash[i].value == i 143 | 144 | # Update an existing value 145 | _hash[4] = 666 146 | assert _hash[4].value == 666 147 | 148 | # Clear the map 149 | for i in range(_hash.capacity()): 150 | del _hash[i] 151 | assert len(_hash) == 0 152 | 153 | # Try to query the empty map 154 | for i in range(_hash.capacity()): 155 | with pytest.raises(KeyError): 156 | _hash[i] 157 | 158 | def test_percpu_hash(skeleton): 159 | """ 160 | Test BPF_PERCPU_HASH. 161 | """ 162 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 163 | 164 | percpu_hash = skel.maps.percpu_hash 165 | 166 | percpu_hash.register_key_type(ct.c_int) 167 | percpu_hash.register_value_type(ct.c_int) 168 | 169 | assert len(percpu_hash) == 0 170 | 171 | init = percpu_hash.ValueType() 172 | for i in range(cpu_count()): 173 | init[i] = i 174 | 175 | for i in range(percpu_hash.capacity()): 176 | percpu_hash[i] = init 177 | 178 | assert len(percpu_hash) == percpu_hash.capacity() 179 | 180 | for i in range(percpu_hash.capacity()): 181 | for j in range(cpu_count()): 182 | assert percpu_hash[i][j] == j 183 | 184 | percpu_hash.clear() 185 | 186 | assert len(percpu_hash) == 0 187 | 188 | def test_lru_hash(skeleton): 189 | """ 190 | Test BPF_LRU_HASH. 191 | """ 192 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 193 | 194 | # Register key and value type 195 | skel.maps.lru_hash.register_key_type(ct.c_int) 196 | skel.maps.lru_hash.register_value_type(ct.c_int) 197 | 198 | lru_hash = skel.maps.lru_hash 199 | 200 | assert len(lru_hash) == 0 201 | 202 | # Try to query the empty map 203 | for i in range(lru_hash.capacity()): 204 | with pytest.raises(KeyError): 205 | lru_hash[i] 206 | 207 | # Overfill the map 208 | for i in range(lru_hash.capacity() + 30): 209 | lru_hash[i] = i 210 | 211 | assert len(lru_hash) <= lru_hash.capacity() 212 | 213 | for i, v in lru_hash.items(): 214 | assert i.value == v.value 215 | 216 | lru_hash.clear() 217 | 218 | assert len(lru_hash) == 0 219 | 220 | def test_lru_percpu_hash(skeleton): 221 | """ 222 | Test BPF_LRU_PERCPU_HASH. 223 | """ 224 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 225 | 226 | lru_percpu_hash = skel.maps.lru_percpu_hash 227 | 228 | lru_percpu_hash.register_key_type(ct.c_int) 229 | lru_percpu_hash.register_value_type(ct.c_int) 230 | 231 | assert len(lru_percpu_hash) == 0 232 | 233 | init = lru_percpu_hash.ValueType() 234 | for i in range(cpu_count()): 235 | init[i] = i 236 | 237 | # Overfill the map 238 | for i in range(lru_percpu_hash.capacity() + 30): 239 | lru_percpu_hash[i] = init 240 | 241 | assert len(lru_percpu_hash) <= lru_percpu_hash.capacity() 242 | 243 | for k, v in lru_percpu_hash.items(): 244 | for j in range(cpu_count()): 245 | assert v[j] == j 246 | 247 | lru_percpu_hash.clear() 248 | 249 | assert len(lru_percpu_hash) == 0 250 | 251 | def test_array(skeleton): 252 | """ 253 | Test BPF_ARRAY. 254 | """ 255 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 256 | 257 | array = skel.maps.array 258 | 259 | # Register value type 260 | array.register_value_type(ct.c_int) 261 | 262 | # It should be impossible to change the key type 263 | with pytest.raises(NotImplementedError): 264 | array.register_key_type(ct.c_int) 265 | 266 | assert len(array) == array.capacity() 267 | 268 | # Try to query the empty map 269 | for i in range(array.capacity()): 270 | array[i] == 0 271 | 272 | # Fill the map 273 | for i in range(array.capacity()): 274 | array[i] = i 275 | assert len(array) == array.capacity() 276 | 277 | # Try to add to full map 278 | with pytest.raises(KeyError): 279 | array[array.capacity()] = 666 280 | 281 | # Query the full map 282 | for i in range(array.capacity()): 283 | assert array[i].value == i 284 | 285 | # Update an existing value 286 | array[4] = 666 287 | assert array[4].value == 666 288 | 289 | # Clear the map 290 | for i in range(array.capacity()): 291 | del array[i] 292 | assert len(array) == array.capacity() 293 | 294 | # Try to query the empty map 295 | for i in range(array.capacity()): 296 | array[i] == 0 297 | 298 | def test_percpu_array(skeleton): 299 | """ 300 | Test BPF_PERCPU_ARRAY. 301 | """ 302 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 303 | 304 | percpu_array = skel.maps.percpu_array 305 | 306 | percpu_array.register_value_type(ct.c_int) 307 | # It should be impossible to change the key type 308 | with pytest.raises(NotImplementedError): 309 | percpu_array.register_key_type(ct.c_int) 310 | 311 | assert len(percpu_array) == percpu_array.capacity() 312 | for i in range(percpu_array.capacity()): 313 | for v in percpu_array[i]: 314 | assert v == 0 315 | 316 | init = percpu_array.ValueType() 317 | for i in range(cpu_count()): 318 | init[i] = i 319 | 320 | for i in range(percpu_array.capacity()): 321 | percpu_array[i] = init 322 | 323 | assert len(percpu_array) == percpu_array.capacity() 324 | 325 | for i in range(percpu_array.capacity()): 326 | for j in range(cpu_count()): 327 | assert percpu_array[i][j] == j 328 | 329 | percpu_array.clear() 330 | 331 | assert len(percpu_array) == percpu_array.capacity() 332 | for i in range(percpu_array.capacity()): 333 | for v in percpu_array[i]: 334 | assert v == 0 335 | 336 | def test_prog_array(skeleton): 337 | """ 338 | Test BPF_PROG_ARRAY. 339 | """ 340 | pytest.skip('TODO') 341 | 342 | def test_perf_event_array(skeleton): 343 | """ 344 | Test BPF_PERF_EVENT_ARRAY. 345 | """ 346 | pytest.skip('TODO') 347 | 348 | def test_stack_trace(skeleton): 349 | """ 350 | Test BPF_STACK_TRACE. 351 | """ 352 | pytest.skip('TODO') 353 | 354 | def test_cgroup_array(skeleton): 355 | """ 356 | Test BPF_CGROUP_ARRAY. 357 | """ 358 | pytest.skip('TODO') 359 | 360 | def test_lpm_trie(skeleton): 361 | """ 362 | Test BPF_LPM_TRIE. 363 | """ 364 | pytest.skip('TODO') 365 | 366 | def test_array_of_maps(skeleton): 367 | """ 368 | Test BPF_ARRAY_OF_MAPS. 369 | """ 370 | pytest.skip('TODO') 371 | 372 | def test_hash_of_maps(skeleton): 373 | """ 374 | Test BPF_HASH_OF_MAPS. 375 | """ 376 | pytest.skip('TODO') 377 | 378 | def test_devmap(skeleton): 379 | """ 380 | Test BPF_DEVMAP. 381 | """ 382 | pytest.skip('TODO') 383 | 384 | def test_sockmap(skeleton): 385 | """ 386 | Test BPF_SOCKMAP. 387 | """ 388 | pytest.skip('TODO') 389 | 390 | def test_cpumap(skeleton): 391 | """ 392 | Test BPF_CPUMAP. 393 | """ 394 | pytest.skip('TODO') 395 | 396 | def test_xskmap(skeleton): 397 | """ 398 | Test BPF_XSKMAP. 399 | """ 400 | pytest.skip('TODO') 401 | 402 | def test_sockhash(skeleton): 403 | """ 404 | Test BPF_SOCKHASH. 405 | """ 406 | pytest.skip('TODO') 407 | 408 | def test_cgroup_storage(skeleton): 409 | """ 410 | Test BPF_CGROUP_STORAGE. 411 | """ 412 | pytest.skip('TODO') 413 | 414 | def test_reuseport_sockarray(skeleton): 415 | """ 416 | Test BPF_REUSEPORT_SOCKARRAY. 417 | """ 418 | pytest.skip('TODO') 419 | 420 | def test_percpu_cgroup_storage(skeleton): 421 | """ 422 | Test BPF_PERCPU_CGROUP_STORAGE. 423 | """ 424 | pytest.skip('TODO') 425 | 426 | def test_queue(skeleton): 427 | """ 428 | Test BPF_QUEUE. 429 | """ 430 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 431 | 432 | queue = skel.maps.queue 433 | 434 | queue.register_value_type(ct.c_int) 435 | 436 | for i in range(queue.capacity()): 437 | queue.push(i) 438 | 439 | with pytest.raises(KeyError): 440 | queue.push(666) 441 | 442 | for i in range(queue.capacity()): 443 | assert queue.peek().value == i 444 | assert queue.peek().value == queue.pop().value 445 | 446 | with pytest.raises(KeyError): 447 | queue.peek() 448 | 449 | with pytest.raises(KeyError): 450 | queue.pop() 451 | 452 | def test_stack(skeleton): 453 | """ 454 | Test BPF_STACK. 455 | """ 456 | skel = skeleton(os.path.join(BPF_SRC, 'maps.bpf.c')) 457 | 458 | stack = skel.maps.stack 459 | 460 | stack.register_value_type(ct.c_int) 461 | 462 | for i in range(stack.capacity()): 463 | stack.push(i) 464 | 465 | with pytest.raises(KeyError): 466 | stack.push(666) 467 | 468 | for i in reversed(range(stack.capacity())): 469 | assert stack.peek().value == i 470 | assert stack.peek().value == stack.pop().value 471 | 472 | with pytest.raises(KeyError): 473 | stack.peek() 474 | 475 | with pytest.raises(KeyError): 476 | stack.pop() 477 | 478 | def test_sk_storage(skeleton): 479 | """ 480 | Test BPF_SK_STORAGE. 481 | """ 482 | pytest.skip('TODO') 483 | 484 | def test_devmap_hash(skeleton): 485 | """ 486 | Test BPF_DEVMAP_HASH. 487 | """ 488 | pytest.skip('TODO') 489 | 490 | def test_struct_ops(skeleton): 491 | """ 492 | Test BPF_STRUCT_OPS. 493 | """ 494 | pytest.skip('TODO') 495 | 496 | -------------------------------------------------------------------------------- /tests/test_progs.py: -------------------------------------------------------------------------------- 1 | """ 2 | pybpf - A BPF CO-RE (Compile Once Run Everywhere) wrapper for Python3 3 | Copyright (C) 2020 William Findlay 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18 | USA 19 | 20 | 2020-Aug-11 William Findlay Created this. 21 | """ 22 | 23 | import os 24 | import time 25 | import subprocess 26 | import ctypes as ct 27 | 28 | import pytest 29 | 30 | from pybpf.maps import create_map 31 | from pybpf.utils import project_path, which 32 | 33 | BPF_SRC = project_path('tests/bpf_src/prog.bpf.c') 34 | XDP_SRC = project_path('tests/bpf_src/xdp.bpf.c') 35 | 36 | def test_progs_smoke(skeleton): 37 | """ 38 | Make sure progs load properly. 39 | """ 40 | skel = skeleton(BPF_SRC) 41 | 42 | EXPECTED_PROG_COUNT = 4 43 | 44 | if len(skel.progs) > EXPECTED_PROG_COUNT: 45 | pytest.xfail(f'EXPECTED_PROG_COUNT should be updated to {len(skel.progs)}') 46 | 47 | assert len(skel.progs) == EXPECTED_PROG_COUNT 48 | 49 | def test_bad_prog(skeleton): 50 | """ 51 | Test that accessing a non-existent prog raises a KeyError. 52 | """ 53 | skel = skeleton(BPF_SRC) 54 | 55 | with pytest.raises(KeyError): 56 | skel.progs.foo 57 | 58 | with pytest.raises(KeyError): 59 | skel.progs['foo'] 60 | 61 | def test_prog_invoke(skeleton): 62 | """ 63 | Test .invoke() method on supported program types. 64 | """ 65 | skel = skeleton(BPF_SRC) 66 | 67 | assert skel.progs.fexit_modify_return_test.invoke() == 0 68 | 69 | assert skel.progs.fentry_modify_return_test.invoke() == 0 70 | 71 | def test_xdp_smoke(skeleton): 72 | """ 73 | Make sure XDP progs load properly. 74 | """ 75 | skel = skeleton(XDP_SRC) 76 | 77 | EXPECTED_PROG_COUNT = 1 78 | 79 | if len(skel.progs) > EXPECTED_PROG_COUNT: 80 | pytest.xfail(f'EXPECTED_PROG_COUNT should be updated to {len(skel.progs)}') 81 | 82 | assert len(skel.progs) == EXPECTED_PROG_COUNT 83 | 84 | skel.progs.xdp_prog.attach_xdp('lo') 85 | skel.maps.packet_count.register_value_type(ct.c_int) 86 | 87 | subprocess.run('ping -c 5 localhost'.split()) 88 | 89 | assert skel.maps.packet_count[0].value > 0 90 | 91 | skel.progs.xdp_prog.remove_xdp('lo') 92 | --------------------------------------------------------------------------------