├── .coveragerc ├── .github └── workflows │ └── pre-commit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── build │ └── .keep ├── make.bat └── source │ ├── api │ ├── blocks.rst │ ├── exceptions.rst │ ├── index.rst │ ├── scanner.rst │ ├── structs.rst │ └── utils.rst │ ├── conf.py │ ├── index.rst │ └── library-usage.rst ├── examples ├── dump_pcapng_info.py ├── dump_pcapng_info_pretty.py ├── dump_tcpip_stats.py ├── generate_pcapng.py └── old │ ├── README.md │ ├── pcapng_to_elasticsearch.py │ └── top-communications.py ├── pcapng-docs └── PCAP Next Generation Dump File Format.html ├── pcapng ├── __init__.py ├── _compat.py ├── blocks.py ├── constants │ ├── __init__.py │ ├── block_types.py │ └── link_types.py ├── exceptions.py ├── flags.py ├── scanner.py ├── strictness.py ├── structs.py ├── utils.py └── writer.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── test_data ├── test001.ntar ├── test002.ntar ├── test003.ntar ├── test004.ntar ├── test005.ntar ├── test006-fixed.ntar ├── test006.ntar ├── test007.ntar ├── test008.ntar ├── test009.ntar └── test010.ntar ├── tests ├── test_parse_enhanced_packet.py ├── test_parse_exceptions.py ├── test_parse_interface_block.py ├── test_parse_interface_stats.py ├── test_parse_section_header.py ├── test_parse_wireshark_capture_files.py ├── test_structs.py ├── test_utils.py └── test_write_support.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = False 3 | source = pcapng 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | pass 9 | raise AssertionError 10 | raise NotImplementedError 11 | show_missing = True 12 | omit = .venv* 13 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Anthony Sottile 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: pre-commit 6 | 7 | on: 8 | pull_request: 9 | push: 10 | branches: [master] 11 | 12 | jobs: 13 | pre-commit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | - uses: pre-commit/action@v2.0.0 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .venv* 4 | /bin 5 | /dist 6 | *.egg 7 | *.egg-info 8 | .coverage 9 | .coverage.* 10 | /.cache 11 | *.pcapng 12 | /.build-venv/ 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/PyCQA/isort 9 | rev: 5.12.0 10 | hooks: 11 | - id: isort 12 | additional_dependencies: 13 | - toml 14 | - repo: https://github.com/psf/black 15 | rev: 23.3.0 16 | hooks: 17 | - id: black 18 | - repo: https://github.com/pycqa/flake8 19 | rev: 6.0.0 20 | hooks: 21 | - id: flake8 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | branches: 6 | except: 7 | - gh-pages 8 | 9 | python: 10 | - "3.5" 11 | - "3.6" 12 | - "3.7" 13 | - "3.8" 14 | - "3.9-dev" 15 | 16 | install: 17 | - pip install .[dev] 18 | 19 | script: 20 | - py.test -vvv --cov=pcapng --cov-report=term-missing tests 21 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | History 2 | ####### 3 | 4 | 2.1.1 5 | ===== 6 | 7 | - Fix deserialization of custom options 8 | 9 | 2.1.0 10 | ===== 11 | 12 | - Use the explicit endianness everywhere when writing. 13 | 14 | 2.0.0 15 | ===== 16 | 17 | - Write support 18 | 19 | v0.1 20 | ==== 21 | 22 | - Support for "scanning" streams of pcap-ng data 23 | - Support for decoding the "standard" pcap-ng blocks: 24 | 25 | - Section headers 26 | - Interface description 27 | - Enhanced packet 28 | - Simple packet (deprecated) 29 | - Packet (deprecated) 30 | - Name resolution 31 | - Interface statistics 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGELOG.rst 3 | # include *.rst 4 | include LICENSE 5 | 6 | # Some examples below. 7 | # See docs at: https://docs.python.org/2/distutils/sourcedist.html#the-manifest-in-template 8 | 9 | # include all files matching any of the listed patterns 10 | # include pat1 pat2 ... 11 | 12 | # exclude all files matching any of the listed patterns 13 | # exclude pat1 pat2 ... 14 | 15 | # include all files under dir matching any of the listed patterns 16 | # recursive-include dir pat1 pat2 ... 17 | 18 | # exclude all files under dir matching any of the listed patterns 19 | # recursive-exclude dir pat1 pat2 ... 20 | 21 | # include all files anywhere in the source tree matching — & any of the listed patterns 22 | # global-include pat1 pat2 ... 23 | 24 | # exclude all files anywhere in the source tree matching — & any of the listed patterns 25 | # global-exclude pat1 pat2 ... 26 | 27 | # exclude all files under dir 28 | # prune dir 29 | 30 | # include all files under dir 31 | # graft dir 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## Standard makefile for Python tests 2 | 3 | BASE_PACKAGE = pcapng 4 | 5 | .PHONY: all upload 6 | 7 | all: help 8 | 9 | help: 10 | @echo "AVAILABLE TARGETS" 11 | @echo "----------------------------------------" 12 | @echo "pypi_upload - build source distribution and upload to pypi" 13 | @echo "pypi_register - register proejct on pypi" 14 | @echo 15 | @echo "install - install project in production mode" 16 | @echo "install_dev - install project in development mode" 17 | @echo 18 | @echo "check (or 'test') - run tests" 19 | @echo "setup_tests - install dependencies for tests" 20 | @echo 21 | @echo "docs - build documentation (HTML)" 22 | @echo "publish_docs - publish documentation to GitHub pages" 23 | 24 | pypi_register: 25 | python setup.py register -r https://pypi.python.org/pypi 26 | 27 | pypi_upload: 28 | python setup.py sdist upload -r https://pypi.python.org/pypi 29 | 30 | install: 31 | python setup.py install 32 | 33 | install_dev: 34 | python setup.py develop 35 | 36 | check: 37 | py.test -vvv --pycodestyle --cov=$(BASE_PACKAGE) --cov-report=term-missing ./tests 38 | 39 | test: check 40 | 41 | setup_tests: 42 | pip install pytest pytest-pycodestyle pytest-cov 43 | 44 | docs: 45 | $(MAKE) -C docs html 46 | 47 | publish_docs: docs 48 | ghp-import -n -p ./docs/build/html 49 | @echo 50 | @echo "HTML output published on github-pages" 51 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python-pcapng 2 | ############# 3 | 4 | Python library to parse the pcap-ng format used by newer versions 5 | of dumpcap & similar tools (wireshark, winpcap, ...). 6 | 7 | 8 | Documentation 9 | ============= 10 | 11 | If you prefer the RTD theme, or want documentation for any version 12 | other than the latest, head here: 13 | 14 | http://python-pcapng.readthedocs.org/en/latest/ 15 | 16 | If you prefer the more comfortable, page-wide, default sphinx theme, 17 | a documentation mirror is hosted on GitHub pages: 18 | 19 | http://rshk.github.io/python-pcapng/ 20 | 21 | 22 | CI build status 23 | =============== 24 | 25 | +----------+--------------------------------------------------------------------------+ 26 | | Branch | Status | 27 | +==========+==========================================================================+ 28 | | master | .. image:: https://travis-ci.org/rshk/python-pcapng.svg?branch=master | 29 | | | :target: https://travis-ci.org/rshk/python-pcapng | 30 | +----------+--------------------------------------------------------------------------+ 31 | | develop | .. image:: https://travis-ci.org/rshk/python-pcapng.svg?branch=develop | 32 | | | :target: https://travis-ci.org/rshk/python-pcapng | 33 | +----------+--------------------------------------------------------------------------+ 34 | 35 | 36 | Source code 37 | =========== 38 | 39 | Source, issue tracker etc. on GitHub: https://github.com/rshk/python-pcapng 40 | 41 | Get the source from git:: 42 | 43 | git clone https://github.com/rshk/python-pcapng 44 | 45 | Download zip of the latest version: 46 | 47 | https://github.com/rshk/python-pcapng/archive/master.zip 48 | 49 | Install from pypi:: 50 | 51 | pip install python-pcapng 52 | 53 | 54 | PyPI status 55 | =========== 56 | 57 | The official page on the Python Package Index is: https://pypi.python.org/pypi/python-pcapng 58 | 59 | .. image:: https://img.shields.io/pypi/v/python-pcapng.svg 60 | :target: https://pypi.python.org/pypi/python-pcapng 61 | :alt: Latest PyPI version 62 | 63 | .. image:: https://img.shields.io/pypi/dm/python-pcapng.svg 64 | :target: https://github.com/rshk/python-pcapng.git 65 | :alt: Number of PyPI downloads 66 | 67 | .. image:: https://img.shields.io/pypi/pyversions/python-pcapng.svg 68 | :target: https://pypi.python.org/pypi/python-pcapng/ 69 | :alt: Supported Python versions 70 | 71 | .. image:: https://img.shields.io/pypi/status/python-pcapng.svg 72 | :target: https://pypi.python.org/pypi/python-pcapng/ 73 | :alt: Development Status 74 | 75 | .. image:: https://img.shields.io/pypi/l/python-pcapng.svg 76 | :target: https://pypi.python.org/pypi/python-pcapng/ 77 | :alt: License 78 | 79 | .. 80 | .. image:: https://pypip.in/wheel/python-pcapng/badge.svg 81 | :target: https://pypi.python.org/pypi/python-pcapng/ 82 | :alt: Wheel Status 83 | 84 | .. image:: https://pypip.in/egg/python-pcapng/badge.svg 85 | :target: https://pypi.python.org/pypi/python-pcapng/ 86 | :alt: Egg Status 87 | 88 | .. image:: https://pypip.in/format/python-pcapng/badge.svg 89 | :target: https://pypi.python.org/pypi/python-pcapng/ 90 | :alt: Download format 91 | 92 | 93 | 94 | Why this library? 95 | ================= 96 | 97 | - I need to decently extract some information from a bunch of pcap-ng 98 | files, but apparently tcpdump has some problems reading those files, 99 | 100 | I couldn't find other nice tools nor Python bindings to a library 101 | able to parse this format, so.. 102 | 103 | - In general, it appears there are (quite a bunch of!) Python modules 104 | to parse the old (much simpler) format, but nothing for the new one. 105 | 106 | - And, they usually completely lack any form of documentation. 107 | 108 | 109 | Isn't it slow? 110 | ============== 111 | 112 | Yes, I guess it would be much slower than something written in C, 113 | but I'm much better at Python than C. 114 | 115 | ..and I need to get things done, and CPU time is not that expensive :) 116 | 117 | (Maybe I'll give a try porting the thing to Cython to speed it up, but 118 | anyways, pure-Python libraries are always useful, eg. for PyPy). 119 | 120 | 121 | How do I use it? 122 | ================ 123 | 124 | Basic usage is as simple as: 125 | 126 | .. code-block:: python 127 | 128 | from pcapng import FileScanner 129 | 130 | with open('/tmp/mycapture.pcap', 'rb') as fp: 131 | scanner = FileScanner(fp) 132 | for block in scanner: 133 | pass # do something with the block... 134 | 135 | Have a look at the blocks documentation to see what they do; also, the 136 | ``examples`` directory contains some example scripts using the library. 137 | 138 | 139 | Hacking 140 | ======= 141 | 142 | Format specification is here: 143 | 144 | https://github.com/pcapng/pcapng/ 145 | 146 | Contributions are welcome, please contact me if you're planning to do 147 | some big change, so that we can sort out the best way to integrate it. 148 | 149 | Or even better, open an issue so the whole world can participate in 150 | the discussion :) 151 | 152 | 153 | Pcap-ng write support 154 | ===================== 155 | 156 | Write support exists as of version 2.0.0. See the file 157 | ``examples/generate_pcapng.py`` for an example of the minimum code 158 | needed to generate a pcapng file. 159 | 160 | In most cases, this library will prevent you from creating broken 161 | data. If you want to create marginal pcapng files, e.g. as test cases 162 | for other software, you can do that by adjusting the "strictness" of 163 | the library, as in: 164 | 165 | .. code-block:: python 166 | 167 | from pcapng.strictness import Strictness, set_strictness 168 | set_strictness(Strictness.FIX) 169 | 170 | Recognized values are ``Strictness.FORBID`` (the default), 171 | ``Strictness.FIX`` (warn about problems, fix *if possible*), 172 | ``Strictness.WARN`` (warn only), and ``Strictness.NONE`` (no warnings). 173 | Circumstances that will result in strictness warnings include: 174 | 175 | * Adding multiples of a non-repeatable option to a block 176 | 177 | * Adding a SPB to a file with more than one interface 178 | 179 | * Writing a PB (PBs are obsolete and not to be used in new files) 180 | 181 | * Writing EPB/SPB/PB/ISB before writing any IDBs 182 | 183 | 184 | Creating a release 185 | ================== 186 | 187 | 1. Create a tag for the new version:: 188 | 189 | git tag v2.0.0 -m 'Version 2.0.0' 190 | 191 | 2. Install build dependencies in a virtualenv:: 192 | 193 | python -m venv ./.build-venv 194 | ./.build-venv/bin/python -m pip install build twine 195 | 196 | 3. Build source and wheel distributions:: 197 | 198 | rm -rf ./dist *.egg-info 199 | ./.build-venv/bin/python -m build 200 | 201 | 4. Use Twine to upload to pypi:: 202 | 203 | twine upload dist/* 204 | 205 | 206 | Troubleshooting 207 | --------------- 208 | 209 | If you get some crazy version number like 210 | ``2.0.1.dev0+g7bd8575.d20220310`` instead of what you expect (eg 211 | ``2.0.0``), it's because you have uncommitted or untracked files in 212 | your local working copy, or you created more commits after creating 213 | the tag. Such a version number will be refused by pypi (and it's not a 214 | good version number anyways), so make sure you have a clean working 215 | copy before building. 216 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.keep 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-pcapng.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-pcapng.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-pcapng" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-pcapng" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/build/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/docs/build/.keep -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-pcapng.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-pcapng.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/api/blocks.rst: -------------------------------------------------------------------------------- 1 | pcapng.blocks 2 | ############# 3 | 4 | .. automodule:: pcapng.blocks 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | pcapng.exceptions 2 | ################# 3 | 4 | .. automodule:: pcapng.exceptions 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ################# 3 | 4 | 5 | Contents: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :glob: 10 | 11 | * 12 | -------------------------------------------------------------------------------- /docs/source/api/scanner.rst: -------------------------------------------------------------------------------- 1 | pcapng.scanner 2 | ############## 3 | 4 | .. automodule:: pcapng.scanner 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/api/structs.rst: -------------------------------------------------------------------------------- 1 | pcapng.structs 2 | ############## 3 | 4 | .. automodule:: pcapng.structs 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/api/utils.rst: -------------------------------------------------------------------------------- 1 | pcapng.utils 2 | ############ 3 | 4 | .. automodule:: pcapng.utils 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-pcapng documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Dec 28 16:37:47 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # import sys 16 | # import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.todo", 34 | "sphinx.ext.viewcode", 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = ".rst" 42 | 43 | # The encoding of source files. 44 | # source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = "index" 48 | 49 | # General information about the project. 50 | project = "python-pcapng" 51 | copyright = "2014, Samuele Santi" 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = "0.1" 59 | # The full version, including alpha/beta/rc tags. 60 | release = "0.1a" 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | # today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | # today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = [] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all 77 | # documents. 78 | # default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | # add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | # add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | # show_authors = False 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = "sphinx" 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | # modindex_common_prefix = [] 96 | 97 | # If true, keep warnings as "system message" paragraphs in the built documents. 98 | # keep_warnings = False 99 | 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | html_theme = "default" 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | # html_theme_options = {} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | # html_theme_path = [] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | # html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | # html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | # html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | # html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ["_static"] 135 | 136 | # Add any extra paths that contain custom files (such as robots.txt or 137 | # .htaccess) here, relative to this directory. These files are copied 138 | # directly to the root of the documentation. 139 | # html_extra_path = [] 140 | 141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 142 | # using the given strftime format. 143 | # html_last_updated_fmt = '%b %d, %Y' 144 | 145 | # If true, SmartyPants will be used to convert quotes and dashes to 146 | # typographically correct entities. 147 | # html_use_smartypants = True 148 | 149 | # Custom sidebar templates, maps document names to template names. 150 | # html_sidebars = {} 151 | 152 | # Additional templates that should be rendered to pages, maps page names to 153 | # template names. 154 | # html_additional_pages = {} 155 | 156 | # If false, no module index is generated. 157 | # html_domain_indices = True 158 | 159 | # If false, no index is generated. 160 | # html_use_index = True 161 | 162 | # If true, the index is split into individual pages for each letter. 163 | # html_split_index = False 164 | 165 | # If true, links to the reST sources are added to the pages. 166 | # html_show_sourcelink = True 167 | 168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 169 | # html_show_sphinx = True 170 | 171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 172 | # html_show_copyright = True 173 | 174 | # If true, an OpenSearch description file will be output, and all pages will 175 | # contain a tag referring to it. The value of this option must be the 176 | # base URL from which the finished HTML is served. 177 | # html_use_opensearch = '' 178 | 179 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 180 | # html_file_suffix = None 181 | 182 | # Output file base name for HTML help builder. 183 | htmlhelp_basename = "python-pcapngdoc" 184 | 185 | 186 | # -- Options for LaTeX output --------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | # 'papersize': 'letterpaper', 191 | # The font size ('10pt', '11pt' or '12pt'). 192 | # 'pointsize': '10pt', 193 | # Additional stuff for the LaTeX preamble. 194 | # 'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | latex_documents = [ 201 | ( 202 | "index", 203 | "python-pcapng.tex", 204 | "python-pcapng Documentation", 205 | "Samuele Santi", 206 | "manual", 207 | ), 208 | ] 209 | 210 | # The name of an image file (relative to this directory) to place at the top of 211 | # the title page. 212 | # latex_logo = None 213 | 214 | # For "manual" documents, if this is true, then toplevel headings are parts, 215 | # not chapters. 216 | # latex_use_parts = False 217 | 218 | # If true, show page references after internal links. 219 | # latex_show_pagerefs = False 220 | 221 | # If true, show URL addresses after external links. 222 | # latex_show_urls = False 223 | 224 | # Documents to append as an appendix to all manuals. 225 | # latex_appendices = [] 226 | 227 | # If false, no module index is generated. 228 | # latex_domain_indices = True 229 | 230 | 231 | # -- Options for manual page output --------------------------------------- 232 | 233 | # One entry per manual page. List of tuples 234 | # (source start file, name, description, authors, manual section). 235 | man_pages = [ 236 | ("index", "python-pcapng", "python-pcapng Documentation", ["Samuele Santi"], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | # man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ( 250 | "index", 251 | "python-pcapng", 252 | "python-pcapng Documentation", 253 | "Samuele Santi", 254 | "python-pcapng", 255 | "One line description of project.", 256 | "Miscellaneous", 257 | ), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | # texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | # texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | # texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | # texinfo_no_detailmenu = False 271 | 272 | 273 | # -- Options for Autodoc -------------------------------------------------- 274 | 275 | autodoc_member_order = "bysource" 276 | 277 | 278 | # -- Options for Todo ----------------------------------------------------- 279 | 280 | todo_include_todos = True 281 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. python-pcapng documentation master file, created by 2 | sphinx-quickstart on Sun Dec 28 16:37:47 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-pcapng's documentation! 7 | ######################################### 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | library-usage 15 | api/index 16 | 17 | 18 | Indices and tables 19 | ################## 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/source/library-usage.rst: -------------------------------------------------------------------------------- 1 | Library usage 2 | ############# 3 | 4 | Use the :py:class:`~pcapng.scanner.FileScanner` class to iterate over blocks 5 | in a pcap-ng archive file, like this: 6 | 7 | .. code-block:: python 8 | 9 | from pcapng import FileScanner 10 | 11 | with open('/tmp/mycapture.pcap') as fp: 12 | scanner = FileScanner(fp) 13 | for block in scanner: 14 | pass # do something with the block... 15 | 16 | Block types can be checked against blocks in :py:mod:`pcapng.blocks`. 17 | -------------------------------------------------------------------------------- /examples/dump_pcapng_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import sys 6 | 7 | import pcapng 8 | 9 | 10 | def dump_information(scanner): 11 | for block in scanner: 12 | print(block) 13 | 14 | 15 | if __name__ == "__main__": 16 | if len(sys.argv) > 1: 17 | with open(sys.argv[1], "rb") as fp: 18 | scanner = pcapng.FileScanner(fp) 19 | dump_information(scanner) 20 | 21 | else: 22 | scanner = pcapng.FileScanner(sys.stdin) 23 | dump_information(scanner) 24 | -------------------------------------------------------------------------------- /examples/dump_pcapng_info_pretty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import sys 5 | from datetime import datetime 6 | 7 | # To make sure all packet types are available 8 | import scapy.all # noqa 9 | import scapy.packet 10 | from scapy.layers.l2 import Ether 11 | 12 | import pcapng 13 | from pcapng.blocks import EnhancedPacket, InterfaceDescription, SectionHeader 14 | 15 | 16 | def col256(text, fg=None, bg=None, bold=False): 17 | def _get_color(col): 18 | return "8;5;{0:d}".format(_to_color(col)) 19 | 20 | def _to_color(num): 21 | if isinstance(num, int): 22 | return num # Assume it is already a color 23 | 24 | if isinstance(num, str) and len(num) <= 3: 25 | return 16 + int(num, 6) 26 | 27 | raise ValueError("Invalid color: {0!r}".format(num)) 28 | 29 | if not isinstance(text, str): 30 | text = repr(text) 31 | 32 | buf = io.StringIO() 33 | 34 | if bold: 35 | buf.write("\x1b[1m") 36 | 37 | if fg is not None: 38 | buf.write("\x1b[3{0}m".format(_get_color(fg))) 39 | 40 | if bg is not None: 41 | buf.write("\x1b[4{0}m".format(_get_color(bg))) 42 | 43 | buf.write(text) 44 | buf.write("\x1b[0m") 45 | return buf.getvalue() 46 | 47 | 48 | def dump_information(scanner): 49 | for block in scanner: 50 | if isinstance(block, SectionHeader): 51 | pprint_sectionheader(block) 52 | elif isinstance(block, InterfaceDescription): 53 | pprint_interfacedesc(block) 54 | elif isinstance(block, EnhancedPacket): 55 | pprint_enhanced_packet(block) 56 | else: 57 | print(" " + str(block)) 58 | 59 | 60 | def pprint_options(options): 61 | if len(options): 62 | yield "--" 63 | for key, values in options.iter_all_items(): 64 | for value in values: 65 | yield col256(key + ":", bold=True, fg="453") 66 | yield col256(str(value), fg="340") 67 | 68 | 69 | def pprint_sectionheader(block): 70 | endianness_desc = { 71 | "<": "Little endian", 72 | ">": "Big endian", 73 | "!": "Network (Big endian)", 74 | "=": "Native", 75 | } 76 | 77 | text = [ 78 | col256(" Section ", bg="400", fg="550"), 79 | col256("version:", bold=True), 80 | col256(".".join(str(x) for x in block.version), fg="145"), 81 | # col256('endianness:', bold=True), 82 | "-", 83 | col256(endianness_desc.get(block.endianness, "Unknown endianness"), bold=True), 84 | "-", 85 | ] 86 | 87 | if block.length < 0: 88 | text.append(col256("unspecified size", bold=True)) 89 | else: 90 | text.append(col256("length:", bold=True)) 91 | text.append(col256(str(block.length), fg="145")) 92 | 93 | text.extend(pprint_options(block.options)) 94 | print(" ".join(text)) 95 | 96 | 97 | def pprint_interfacedesc(block): 98 | text = [ 99 | col256(" Interface #{0} ".format(block.interface_id), bg="010", fg="453"), 100 | col256("Link type:", bold=True), 101 | col256(str(block.link_type), fg="140"), 102 | col256(block.link_type_description, fg="145"), 103 | col256("Snap length:", bold=True), 104 | col256(str(block.snaplen), fg="145"), 105 | ] 106 | text.extend(pprint_options(block.options)) 107 | print(" ".join(text)) 108 | 109 | 110 | def pprint_enhanced_packet(block): 111 | text = [ 112 | col256(" Packet+ ", bg="001", fg="345"), 113 | # col256('NIC:', bold=True), 114 | # col256(str(block.interface_id), fg='145'), 115 | col256(str(block.interface.options["if_name"]), fg="140"), 116 | col256( 117 | str( 118 | datetime.utcfromtimestamp(block.timestamp).strftime("%Y-%m-%d %H:%M:%S") 119 | ), 120 | fg="455", 121 | ), 122 | ] 123 | try: 124 | text.extend( 125 | [ 126 | col256("NIC:", bold=True), 127 | col256(block.interface_id, fg="145"), 128 | col256(block.interface.options["if_name"], fg="140"), 129 | ] 130 | ) 131 | except KeyError: 132 | pass 133 | 134 | text.extend( 135 | [ 136 | # col256('Size:', bold=True), 137 | col256(str(block.packet_len) + " bytes", fg="025") 138 | ] 139 | ) 140 | 141 | if block.captured_len != block.packet_len: 142 | text.extend( 143 | [ 144 | col256("Truncated to:", bold=True), 145 | col256(str(block.captured_len) + "bytes", fg="145"), 146 | ] 147 | ) 148 | 149 | text.extend(pprint_options(block.options)) 150 | print(" ".join(text)) 151 | 152 | if block.interface.link_type == 1: 153 | # print(repr(block.packet_data)) 154 | # print(col256(repr(Ether(block.packet_data)), fg='255')) 155 | 156 | _info = format_packet_information(block.packet_data) 157 | print("\n".join(" " + line for line in _info)) 158 | 159 | else: 160 | print(" Printing information for non-ethernet packets") 161 | print(" is not supported yet.") 162 | 163 | # print('\n'.join(' ' + line 164 | # for line in format_binary_data(block.packet_data))) 165 | 166 | 167 | def format_packet_information(packet_data): 168 | decoded = Ether(packet_data) 169 | return format_scapy_packet(decoded) 170 | 171 | 172 | def format_scapy_packet(packet): 173 | fields = [] 174 | for f in packet.fields_desc: 175 | # if isinstance(f, ConditionalField) and not f._evalcond(self): 176 | # continue 177 | if f.name in packet.fields: 178 | val = f.i2repr(packet, packet.fields[f.name]) 179 | 180 | elif f.name in packet.overloaded_fields: 181 | val = f.i2repr(packet, packet.overloaded_fields[f.name]) 182 | 183 | else: 184 | continue 185 | 186 | fields.append("{0}={1}".format(col256(f.name, "542"), col256(val, "352"))) 187 | 188 | yield "{0} {1}".format(col256(packet.__class__.__name__, "501"), " ".join(fields)) 189 | 190 | if packet.payload: 191 | if isinstance(packet.payload, scapy.packet.Raw): 192 | raw_data = str(packet.payload) 193 | for line in make_printable(raw_data).splitlines(): 194 | yield " " + line 195 | 196 | # for line in format_binary_data(raw_data): 197 | # yield ' ' + line 198 | 199 | elif isinstance(packet.payload, scapy.packet.Packet): 200 | for line in format_scapy_packet(packet.payload): 201 | yield " " + line 202 | 203 | else: 204 | for line in repr(packet.payload).splitlines(): 205 | yield " " + line 206 | 207 | 208 | def make_printable(data): # todo: preserve unicode 209 | stream = io.StringIO() 210 | for ch in data: 211 | if ch == "\\": 212 | stream.write("\\\\") 213 | elif ch in "\n\r" or (32 <= ord(ch) <= 126): 214 | stream.write(ch) 215 | else: 216 | stream.write("\\x{0:02x}".format(ord(ch))) 217 | return stream.getvalue() 218 | 219 | 220 | def format_binary_data(data): 221 | stream = io.BytesIO(data) 222 | row_offset = 0 223 | row_size = 16 # bytes 224 | 225 | while True: 226 | data = stream.read(row_size) 227 | if not data: 228 | return 229 | 230 | hexrow = io.BytesIO() 231 | asciirow = io.BytesIO() 232 | for i, byte in enumerate(data): 233 | if 32 <= ord(byte) <= 126: 234 | asciirow.write(byte) 235 | else: 236 | asciirow.write(".") 237 | hexrow.write(format(ord(byte), "02x")) 238 | if i < 15: 239 | if i % 2 == 1: 240 | hexrow.write(" ") 241 | if i % 8 == 7: 242 | hexrow.write(" ") 243 | 244 | row_offset += 1 245 | 246 | yield "{0:08x}: {1:40s} {2:16s}".format( 247 | row_offset, hexrow.getvalue(), asciirow.getvalue() 248 | ) 249 | 250 | 251 | def main(): 252 | if (len(sys.argv) > 1) and (sys.argv[1] != "-"): 253 | with open(sys.argv[1], "rb") as fp: 254 | scanner = pcapng.FileScanner(fp) 255 | dump_information(scanner) 256 | else: 257 | scanner = pcapng.FileScanner(sys.stdin) 258 | dump_information(scanner) 259 | 260 | 261 | if __name__ == "__main__": 262 | main() 263 | -------------------------------------------------------------------------------- /examples/dump_tcpip_stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division, print_function 4 | 5 | import logging 6 | import sys 7 | from collections import Counter 8 | 9 | from scapy.layers.inet import IP, TCP 10 | from scapy.layers.l2 import Ether 11 | 12 | from pcapng import FileScanner 13 | from pcapng.blocks import EnhancedPacket 14 | 15 | logger = logging.getLogger("pcapng") 16 | logger.setLevel(logging.INFO) # Debug will slow things down a lot! 17 | 18 | handler = logging.StreamHandler(sys.stderr) 19 | formatter = logging.Formatter( 20 | "\033[1;37;40m %(levelname)s \033[0m \033[0;32m%(message)s\033[0m" 21 | ) 22 | handler.setFormatter(formatter) 23 | logger.addHandler(handler) 24 | 25 | 26 | def title(text): 27 | print("-" * 60) 28 | print("\033[1m{0}\033[0m".format(text)) 29 | print("-" * 60) 30 | 31 | 32 | def human_number(num, k=1000): 33 | powers = [""] + list("kMGTPEY") 34 | assert isinstance(num, int) 35 | for i, suffix in enumerate(powers): 36 | if (num < (k ** (i + 1))) or (i == len(powers) - 1): 37 | return "{0:d}{1}".format(int(round(num / (k**i))), suffix) 38 | raise AssertionError("Should never reach this") 39 | 40 | 41 | if __name__ == "__main__": 42 | import sys 43 | 44 | rdr = FileScanner(sys.stdin) 45 | 46 | ip_src_count = Counter() 47 | ip_dst_count = Counter() 48 | ip_src_size = Counter() 49 | ip_dst_size = Counter() 50 | 51 | tcp_src_count = Counter() 52 | tcp_dst_count = Counter() 53 | tcp_src_size = Counter() 54 | tcp_dst_size = Counter() 55 | 56 | for block in rdr: 57 | # print(repr(block)) 58 | 59 | if isinstance(block, EnhancedPacket): 60 | assert block.interface.link_type == 1 # must be ethernet! 61 | 62 | decoded = Ether(block.packet_data) 63 | # print(repr(Ether(block.packet_data))[:400] + '...') 64 | 65 | _pl1 = decoded.payload 66 | if isinstance(_pl1, IP): 67 | ip_src_count[_pl1.src] += 1 68 | ip_dst_count[_pl1.dst] += 1 69 | ip_src_size[_pl1.src] += block.packet_len 70 | ip_dst_size[_pl1.dst] += block.packet_len 71 | 72 | _pl2 = _pl1.payload 73 | if isinstance(_pl2, TCP): 74 | _src = "{0}:{1}".format(_pl1.dst, _pl2.dport) 75 | _dst = "{0}:{1}".format(_pl1.src, _pl2.sport) 76 | tcp_src_count[_src] += 1 77 | tcp_dst_count[_dst] += 1 78 | tcp_src_size[_src] += block.packet_len 79 | tcp_dst_size[_dst] += block.packet_len 80 | 81 | # Print report 82 | # ------------------------------------------------------------ 83 | 84 | def _rsic(o): 85 | return sorted(o.items(), key=lambda x: x[1], reverse=True) 86 | 87 | title("IP Sources (by packet count)") 88 | for key, val in _rsic(ip_src_count)[:30]: 89 | print("\033[1m{1:>5s}\033[0m {0}".format(key, human_number(val))) 90 | print() 91 | 92 | title("IP Sources (by total size)") 93 | for key, val in _rsic(ip_src_size)[:30]: 94 | print("\033[1m{1:>5s}B\033[0m {0}".format(key, human_number(val, k=1024))) 95 | print() 96 | 97 | title("IP Destinations (by packet count)") 98 | for key, val in _rsic(ip_dst_count)[:30]: 99 | print("\033[1m{1:>5s}\033[0m {0}".format(key, human_number(val))) 100 | print() 101 | 102 | title("IP Destinations (by total size)") 103 | for key, val in _rsic(ip_dst_size)[:30]: 104 | print("\033[1m{1:>5s}B\033[0m {0}".format(key, human_number(val, k=1024))) 105 | print() 106 | 107 | title("TCP Sources (by packet count)") 108 | for key, val in _rsic(tcp_src_count)[:30]: 109 | print("\033[1m{1:>5s}\033[0m {0}".format(key, human_number(val))) 110 | print() 111 | 112 | title("TCP Sources (by total size)") 113 | for key, val in _rsic(tcp_src_size)[:30]: 114 | print("\033[1m{1:>5s}B\033[0m {0}".format(key, human_number(val, k=1024))) 115 | print() 116 | 117 | title("TCP Destinations (by packet count)") 118 | for key, val in _rsic(tcp_dst_count)[:30]: 119 | print("\033[1m{1:>5s}\033[0m {0}".format(key, human_number(val))) 120 | print() 121 | 122 | title("TCP Destinations (by total size)") 123 | for key, val in _rsic(tcp_dst_size)[:30]: 124 | print("\033[1m{1:>5s}B\033[0m {0}".format(key, human_number(val, k=1024))) 125 | print() 126 | -------------------------------------------------------------------------------- /examples/generate_pcapng.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | 5 | import pcapng.blocks as blocks 6 | from pcapng import FileWriter 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("outfile", type=argparse.FileType("wb")) 10 | args = parser.parse_args() 11 | 12 | shb = blocks.SectionHeader( 13 | options={ 14 | "shb_hardware": "artificial", 15 | "shb_os": "python", 16 | "shb_userappl": "python-pcapng", 17 | } 18 | ) 19 | idb = shb.new_member( 20 | blocks.InterfaceDescription, 21 | link_type=1, 22 | options={ 23 | "if_description": "Hand-rolled", 24 | "if_os": "Python", 25 | "if_filter": [(0, b"tcp port 23 and host 192.0.2.5")], 26 | }, 27 | ) 28 | 29 | # FileWriter() immediately writes the SHB and any IDBs you've added to it 30 | writer = FileWriter(args.outfile, shb) 31 | 32 | # fmt: off 33 | test_pl = ( 34 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, # dest MAC 35 | 0x11, 0x22, 0x33, 0xdd, 0xaa, 0x00, # src MAC 36 | 0x08, 0x00, # ethertype (ipv4) 37 | 0x45, 0x00, 0x00, 31, # IP start 38 | 0x00, 0x00, 0x00, 0x00, # ID+flags 39 | 0xfe, 17, # TTL, UDP 40 | 0x00, 0x00, # checksum 41 | 127, 0, 0, 1, # src IP 42 | 127, 0, 0, 2, # dst IP 43 | 0x12, 0x34, 0x56, 0x78, # src/dst ports 44 | 0x00, 11, # length 45 | 0x00, 0x00, # checksum 46 | 0x44, 0x41, 0x50, # Payload 47 | ) 48 | # fmt: on 49 | 50 | spb = shb.new_member(blocks.SimplePacket) 51 | spb.packet_data = bytes(test_pl) 52 | writer.write_block(spb) 53 | -------------------------------------------------------------------------------- /examples/old/README.md: -------------------------------------------------------------------------------- 1 | # Old examples 2 | 3 | They are ok but still need porting to refactored version of the library. 4 | -------------------------------------------------------------------------------- /examples/old/pcapng_to_elasticsearch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convert a pcap-ng file to a format suitable for bulk insert 3 | in elasticsearch. 4 | 5 | Example: 6 | 7 | 8 | curl -XPUT localhost:9200/net-traffic -d '{"packet": { 9 | "properties": { 10 | "@tymestamp": {"type": "date"} 11 | }, 12 | "dynamic_templates": [{ 13 | "packet_fields_as_string": { 14 | "path_match": "*.*", 15 | "mapping": { 16 | "type": "string", 17 | "index": "not_analyzed" 18 | } 19 | } 20 | }] 21 | }}' 22 | 23 | ./pcapng_to_elasticsearch.py < capture.pcapng > capture.json 24 | 25 | curl -XPUT localhost:9200/net-traffic/_bulk --data-binary @capture.json 26 | """ 27 | 28 | from __future__ import division, print_function 29 | 30 | import hashlib 31 | import json 32 | import logging 33 | import sys 34 | 35 | from scapy.layers.all import * # Needed for decode! # noqa 36 | from scapy.layers.l2 import Ether 37 | from scapy.packet import Raw 38 | 39 | from pcapng import PcapngReader 40 | from pcapng.objects import EnhancedPacket 41 | 42 | 43 | class SaferJsonEncoder(json.JSONEncoder): 44 | def default(self, obj): 45 | try: 46 | return json.JSONEncoder.default(self, obj) 47 | except TypeError: 48 | return repr(obj) 49 | 50 | 51 | pcapng_logger = logging.getLogger("pcapng") 52 | pcapng_logger.setLevel(logging.INFO) 53 | 54 | logger = logging.getLogger(__name__) 55 | logger.setLevel(logging.INFO) 56 | 57 | handler = logging.StreamHandler(sys.stderr) 58 | formatter = logging.Formatter( 59 | "\033[1;37;40m %(levelname)s \033[0m \033[0;32m%(message)s\033[0m" 60 | ) 61 | handler.setFormatter(formatter) 62 | 63 | pcapng_logger.addHandler(handler) 64 | logger.addHandler(handler) 65 | 66 | 67 | if __name__ == "__main__": 68 | import sys 69 | 70 | rdr = PcapngReader(sys.stdin) 71 | 72 | def _find_layers(pkt): 73 | # Iterating pkt is quite a confused thing.. 74 | # Another options would be getting layers as pkt[id] 75 | while True: 76 | yield pkt 77 | if not pkt.payload: 78 | return 79 | pkt = pkt.payload 80 | 81 | for block in rdr: 82 | # print(repr(block)) 83 | 84 | if isinstance(block, EnhancedPacket): 85 | # We expect only ethernet packets in this dump 86 | assert block._interface.link_type == 1 87 | 88 | # We need to figure out a unique id for this packet, 89 | # in a deterministic way (in case we re-import the 90 | # same batch..) 91 | # Hopefully, this will be unique.. 92 | packet_id = hashlib.sha1( 93 | str(block.timestamp) + block.packet_data 94 | ).hexdigest() 95 | 96 | packet = Ether(block.packet_data) # Decode packet data 97 | 98 | logger.info( 99 | "Processing packet {0}: {1}".format(packet_id, repr(packet)[:200]) 100 | ) 101 | 102 | packet_record = { 103 | "@timestamp": block.timestamp * 1000, # in milliseconds 104 | "packet_size": block.packet_len, 105 | # todo: add information about interface, etc? 106 | } 107 | 108 | for pkt in _find_layers(packet): 109 | if isinstance(pkt, Raw): 110 | # Ignore raw packet contents! 111 | continue 112 | packet_record[pkt.name] = pkt.fields 113 | 114 | try: 115 | _pkt_json = json.dumps(packet_record, cls=SaferJsonEncoder) 116 | 117 | except Exception: 118 | logger.exception("Unable to serialize json packet") 119 | 120 | else: 121 | print(json.dumps({"index": {"_type": "packet", "_id": packet_id}})) 122 | print(_pkt_json) 123 | -------------------------------------------------------------------------------- /examples/old/top-communications.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find communications using the most traffic 3 | """ 4 | 5 | from __future__ import print_function 6 | 7 | import logging 8 | import sys 9 | from collections import Counter, defaultdict 10 | 11 | from scapy.layers.inet import IP, TCP, UDP 12 | from scapy.layers.l2 import Ether 13 | 14 | from pcapng import PcapngReader 15 | from pcapng.objects import EnhancedPacket 16 | 17 | logger = logging.getLogger("pcapng") 18 | logger.setLevel(logging.INFO) # Debug will slow things down a lot! 19 | 20 | handler = logging.StreamHandler(sys.stderr) 21 | formatter = logging.Formatter( 22 | "\033[1;37;40m %(levelname)s \033[0m \033[0;32m%(message)s\033[0m" 23 | ) 24 | handler.setFormatter(formatter) 25 | logger.addHandler(handler) 26 | 27 | 28 | if __name__ == "__main__": 29 | import sys 30 | 31 | rdr = PcapngReader(sys.stdin) 32 | 33 | counters = defaultdict(Counter) 34 | 35 | for block in rdr: 36 | # print(repr(block)) 37 | 38 | if isinstance(block, EnhancedPacket): 39 | # We expect only ethernet packets in this dump 40 | assert block._interface.link_type == 1 41 | 42 | packet = Ether(block.packet_data) 43 | # print(repr(Ether(block.packet_data))[:400] + '...') 44 | _pksize = block.packet_len 45 | 46 | if IP in packet: 47 | if TCP in packet: 48 | # TCP packet 49 | _tcp_com = ( 50 | packet[IP].src, 51 | packet[TCP].sport, 52 | packet[IP].dst, 53 | packet[TCP].dport, 54 | ) 55 | counters["TCP Communications (count)"][_tcp_com] += 1 56 | counters["TCP Communications (size)"][_tcp_com] += _pksize 57 | 58 | elif UDP in packet: 59 | # UDP packet 60 | _udp_com = ( 61 | packet[IP].src, 62 | packet[UDP].sport, 63 | packet[IP].dst, 64 | packet[UDP].dport, 65 | ) 66 | counters["UDP Communications (count)"][_udp_com] += 1 67 | counters["UDP Communications (size)"][_udp_com] += _pksize 68 | 69 | else: 70 | # Generic IP packet 71 | _ip_com = (packet[IP].src, packet[IP].dst) 72 | counters["Other IP Communications (count)"][_ip_com] += 1 73 | counters["Other IP Communications (size)"][ 74 | _ip_com 75 | ] += _pksize # noqa 76 | 77 | else: 78 | counters["Non-IP packets (size)"]["total"] += _pksize 79 | counters["Non-IP packets (count)"]["total"] += 1 80 | 81 | # Print report 82 | # ------------------------------------------------------------ 83 | 84 | for counter_name, items in sorted(counters.iteritems()): 85 | print(counter_name) 86 | print("-" * 60) 87 | sorted_items = sorted(items.iteritems(), key=lambda x: x[1], reverse=True) 88 | 89 | for key, value in sorted_items[:30]: 90 | print("{1:15d} {0}".format(key, value)) 91 | 92 | print() 93 | -------------------------------------------------------------------------------- /pcapng/__init__.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------- 2 | # Library to parse pcap-ng file format 3 | # 4 | # See: http://www.winpcap.org/ntar/draft/PCAP-DumpFileFormat.html 5 | # ---------------------------------------------------------------------- 6 | 7 | from .scanner import FileScanner # noqa 8 | from .writer import FileWriter # noqa 9 | -------------------------------------------------------------------------------- /pcapng/_compat.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple as _namedtuple 2 | 3 | 4 | # version-portable namedtuple with defaults 5 | def namedtuple(typename, field_names, defaults=None): 6 | if not defaults: 7 | # No defaults given or needed 8 | return _namedtuple(typename, field_names) 9 | try: 10 | # Python 3.7+ 11 | return _namedtuple(typename, field_names, defaults=defaults) 12 | except TypeError: 13 | T = _namedtuple(typename, field_names) 14 | # Python 2.7, up to 3.6 15 | T.__new__.__defaults__ = defaults 16 | return T 17 | -------------------------------------------------------------------------------- /pcapng/blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module containing the definition of known / supported "blocks" of the 3 | pcap-ng format. 4 | 5 | Each block is a struct-like object with some fields and possibly 6 | a variable amount of "items" (usually options). 7 | 8 | They can optionally expose some other properties, used eg. to provide 9 | better access to decoded information, ... 10 | """ 11 | 12 | 13 | import io 14 | import itertools 15 | 16 | from pcapng import strictness as strictness 17 | from pcapng.constants import link_types 18 | from pcapng.structs import ( 19 | IntField, 20 | ListField, 21 | NameResolutionRecordField, 22 | Option, 23 | Options, 24 | OptionsField, 25 | PacketBytes, 26 | RawBytes, 27 | struct_decode, 28 | write_bytes_padded, 29 | write_int, 30 | ) 31 | from pcapng.utils import unpack_timestamp_resolution 32 | 33 | KNOWN_BLOCKS = {} 34 | 35 | 36 | class Block(object): 37 | """Base class for blocks""" 38 | 39 | schema = [] 40 | readonly_fields = set() 41 | __slots__ = [ 42 | # These are in addition to the above two properties 43 | "magic_number", 44 | "_decoded", 45 | ] 46 | 47 | def __init__(self, **kwargs): 48 | if "raw" in kwargs: 49 | self._decoded = struct_decode( 50 | self.schema, io.BytesIO(kwargs["raw"]), kwargs["endianness"] 51 | ) 52 | else: 53 | self._decoded = {} 54 | for key, packed_type, default in self.schema: 55 | if key == "options": 56 | self._decoded[key] = Options( 57 | schema=packed_type.options_schema, 58 | data={}, 59 | endianness=kwargs["endianness"], 60 | ) 61 | if "options" in kwargs: 62 | for oky, ovl in kwargs["options"].items(): 63 | self.options[oky] = ovl 64 | else: 65 | try: 66 | self._decoded[key] = kwargs[key] 67 | except KeyError: 68 | self._decoded[key] = default 69 | 70 | def __eq__(self, other): 71 | if self.__class__ != other.__class__: 72 | return False 73 | keys = [x[0] for x in self.schema] 74 | # Use `getattr()` so eg. @property calls are used 75 | return [getattr(self, k) for k in keys] == [getattr(other, k) for k in keys] 76 | 77 | def _write(self, outstream): 78 | """Writes this block into the given output stream""" 79 | encoded_block = io.BytesIO() 80 | self._encode(encoded_block) 81 | encoded_block = encoded_block.getvalue() 82 | subblock_length = len(encoded_block) 83 | if subblock_length == 0: 84 | return 85 | block_length = 12 + subblock_length 86 | if subblock_length % 4 != 0: 87 | block_length += 4 - (subblock_length % 4) 88 | write_int(self.magic_number, outstream, 32, endianness=self.section.endianness) 89 | write_int(block_length, outstream, 32, endianness=self.section.endianness) 90 | write_bytes_padded(outstream, encoded_block) 91 | write_int(block_length, outstream, 32, endianness=self.section.endianness) 92 | 93 | def _encode(self, outstream): 94 | """Encodes the fields of this block into raw data""" 95 | for name, field, default in self.schema: 96 | field.encode( 97 | getattr(self, name), outstream, endianness=self.section.endianness 98 | ) 99 | 100 | def __getattr__(self, name): 101 | # __getattr__ is only called when getting an attribute that 102 | # this object doesn't have. 103 | try: 104 | return self._decoded[name] 105 | except KeyError as e: 106 | raise AttributeError(name) from e 107 | 108 | def __setattr__(self, name, value): 109 | # __setattr__ is called for *any* attribute, real or not. 110 | try: 111 | # See it if it names an attribute we defined in our __slots__ 112 | return object.__setattr__(self, name, value) 113 | except AttributeError: 114 | # It's not an attribute. 115 | # Proceed with our nice interface to the schema. 116 | pass 117 | if name in self.readonly_fields: 118 | raise AttributeError( 119 | "'{cls}' object property '{prop}' is read-only".format( 120 | prop=name, cls=self.__class__.__name__ 121 | ) 122 | ) 123 | if self._decoded is None: 124 | self._decoded = {} 125 | for key, packed_type, default in self.schema: 126 | self._decoded[key] = None 127 | self._decoded[name] = value 128 | 129 | def __repr__(self): 130 | args = [] 131 | for item in self.schema: 132 | name = item[0] 133 | value = getattr(self, name) 134 | try: 135 | value = repr(value) 136 | except Exception: 137 | value = "<{0} (repr failed)>".format(type(value).__name__) 138 | args.append("{0}={1}".format(name, value)) 139 | return "<{0} {1}>".format(self.__class__.__name__, " ".join(args)) 140 | 141 | 142 | class SectionMemberBlock(Block): 143 | """Block which must be a member of a section""" 144 | 145 | __slots__ = ["section"] 146 | 147 | def __init__(self, section, **kwargs): 148 | super(SectionMemberBlock, self).__init__(**kwargs) 149 | self.section = section 150 | 151 | 152 | def register_block(block): 153 | """Handy decorator to register a new known block type""" 154 | KNOWN_BLOCKS[block.magic_number] = block 155 | return block 156 | 157 | 158 | @register_block 159 | class SectionHeader(Block): 160 | """ 161 | "The Section Header Block (SHB) is mandatory. It identifies the beginning 162 | of a section of the capture file. The Section Header Block does not contain 163 | data but it rather identifies a list of blocks (interfaces, packets) that 164 | are logically correlated." 165 | - pcapng spec, section 4.1. Other quoted citations are from this section 166 | unless otherwise noted. 167 | """ 168 | 169 | magic_number = 0x0A0D0D0A 170 | __slots__ = [ 171 | "endianness", 172 | "_interfaces_id", 173 | "interfaces", 174 | "interface_stats", 175 | ] 176 | schema = [ 177 | ("version_major", IntField(16, False), 1), 178 | ("version_minor", IntField(16, False), 0), 179 | ("section_length", IntField(64, True), -1), 180 | ( 181 | "options", 182 | OptionsField( 183 | [ 184 | Option(2, "shb_hardware", "string"), 185 | Option(3, "shb_os", "string"), 186 | Option(4, "shb_userappl", "string"), 187 | ] 188 | ), 189 | None, 190 | ), 191 | ] 192 | 193 | def __init__(self, endianness="<", **kwargs): 194 | self.endianness = endianness 195 | self._interfaces_id = itertools.count(0) 196 | self.interfaces = {} 197 | self.interface_stats = {} 198 | super(SectionHeader, self).__init__(endianness=endianness, **kwargs) 199 | 200 | def _encode(self, outstream): 201 | write_int(0x1A2B3C4D, outstream, 32, endianness=self.endianness) 202 | super(SectionHeader, self)._encode(outstream) 203 | 204 | def new_member(self, cls, **kwargs): 205 | """Helper method to create a block that's a member of this section""" 206 | assert issubclass(cls, SectionMemberBlock) 207 | blk = cls(section=self, endianness=self.endianness, **kwargs) 208 | # Some blocks (eg. SPB) don't have options 209 | if any([x[0] == "options" for x in blk.schema]): 210 | blk.options.endianness = self.endianness 211 | if isinstance(blk, InterfaceDescription): 212 | self.register_interface(blk) 213 | elif isinstance(blk, InterfaceStatistics): 214 | self.add_interface_stats(blk) 215 | return blk 216 | 217 | def register_interface(self, interface): 218 | """Helper method to register an interface within this section""" 219 | assert isinstance(interface, InterfaceDescription) 220 | interface_id = next(self._interfaces_id) 221 | interface.interface_id = interface_id 222 | self.interfaces[interface_id] = interface 223 | 224 | def add_interface_stats(self, interface_stats): 225 | """Helper method to register interface stats within this section""" 226 | assert isinstance(interface_stats, InterfaceStatistics) 227 | self.interface_stats[interface_stats.interface_id] = interface_stats 228 | 229 | @property 230 | def version(self): 231 | return (self.version_major, self.version_minor) 232 | 233 | @property 234 | def length(self): 235 | return self.section_length 236 | 237 | # Block.decode() assumes all blocks have sections -- technically true... 238 | @property 239 | def section(self): 240 | return self 241 | 242 | def __repr__(self): 243 | return ( 244 | "<{name} version={version} endianness={endianness} " 245 | "length={length} options={options}>" 246 | ).format( 247 | name=self.__class__.__name__, 248 | version=".".join(str(x) for x in self.version), 249 | endianness=repr(self.endianness), 250 | length=self.length, 251 | options=repr(self.options), 252 | ) 253 | 254 | 255 | @register_block 256 | class InterfaceDescription(SectionMemberBlock): 257 | """ 258 | "An Interface Description Block (IDB) is the container for information 259 | describing an interface on which packet data is captured." 260 | - pcapng spec, section 4.2. Other quoted citations are from this section 261 | unless otherwise noted. 262 | """ 263 | 264 | magic_number = 0x00000001 265 | __slots__ = ["interface_id"] 266 | schema = [ 267 | ("link_type", IntField(16, False), 0), # todo: enc/decode 268 | ("reserved", IntField(16, False), 0), 269 | ("snaplen", IntField(32, False), 0), 270 | ( 271 | "options", 272 | OptionsField( 273 | [ 274 | Option(2, "if_name", "string"), 275 | Option(3, "if_description", "string"), 276 | Option(4, "if_IPv4addr", "ipv4+mask", multiple=True), 277 | Option(5, "if_IPv6addr", "ipv6+prefix", multiple=True), 278 | Option(6, "if_MACaddr", "macaddr"), 279 | Option(7, "if_EUIaddr", "euiaddr"), 280 | Option(8, "if_speed", "u64"), 281 | Option(9, "if_tsresol"), # Just keep the raw data 282 | Option(10, "if_tzone", "u32"), 283 | Option(11, "if_filter", "type+bytes"), 284 | Option(12, "if_os", "string"), 285 | Option(13, "if_fcslen", "u8"), 286 | Option(14, "if_tsoffset", "i64"), 287 | Option(15, "if_hardware", "string"), 288 | Option(16, "if_txspeed", "u64"), 289 | Option(17, "if_rxspeed", "u64"), 290 | ] 291 | ), 292 | None, 293 | ), 294 | ] 295 | 296 | @property # todo: cache this property 297 | def timestamp_resolution(self): 298 | # ------------------------------------------------------------ 299 | # Resolution of timestamps. If the Most Significant Bit is 300 | # equal to zero, the remaining bits indicates the resolution 301 | # of the timestamp as as a negative power of 10 (e.g. 6 means 302 | # microsecond resolution, timestamps are the number of 303 | # microseconds since 1/1/1970). If the Most Significant Bit is 304 | # equal to one, the remaining bits indicates the resolution as 305 | # as negative power of 2 (e.g. 10 means 1/1024 of second). If 306 | # this option is not present, a resolution of 10^-6 is assumed 307 | # (i.e. timestamps have the same resolution of the standard 308 | # 'libpcap' timestamps). 309 | # ------------------------------------------------------------ 310 | 311 | if "if_tsresol" in self.options: 312 | return unpack_timestamp_resolution(self.options["if_tsresol"]) 313 | 314 | return 1e-6 315 | 316 | @property 317 | def statistics(self): 318 | # todo: ensure we always have an interface id -> how?? 319 | return self.section.interface_stats.get(self.interface_id) 320 | 321 | @property 322 | def link_type_description(self): 323 | try: 324 | return link_types.LINKTYPE_DESCRIPTIONS[self.link_type] 325 | except KeyError: 326 | return "Unknown link type: 0x{0:04x}".format(self.link_type) 327 | 328 | 329 | class BlockWithTimestampMixin(object): 330 | """ 331 | Block mixin adding properties to better access timestamps 332 | of blocks that provide one. 333 | """ 334 | 335 | __slots__ = [] 336 | 337 | @property 338 | def timestamp(self): 339 | # First, get the accuracy from the ts_resol option 340 | return ( 341 | (self.timestamp_high << 32) + self.timestamp_low 342 | ) * self.timestamp_resolution 343 | 344 | @property 345 | def timestamp_resolution(self): 346 | return self.interface.timestamp_resolution 347 | 348 | # todo: add some property returning a datetime() with timezone.. 349 | 350 | 351 | class BlockWithInterfaceMixin(object): 352 | """ 353 | Block mixin for blocks that have/require an interface. 354 | This includes all packet blocks as well as InterfaceStatistics. 355 | """ 356 | 357 | __slots__ = [] 358 | 359 | @property 360 | def interface(self): 361 | # We need to get the correct interface from the section 362 | # by looking up the interface_id 363 | return self.section.interfaces[self.interface_id] 364 | 365 | def _encode(self, outstream): 366 | if len(self.section.interfaces) < 1: 367 | strictness.problem( 368 | "writing {cls} for section with no interfaces".format( 369 | cls=self.__class__.__name__ 370 | ) 371 | ) 372 | if strictness.should_fix(): 373 | # Only way to "fix" is to not write the block 374 | return 375 | super(BlockWithInterfaceMixin, self)._encode(outstream) 376 | 377 | 378 | class BasePacketBlock(SectionMemberBlock, BlockWithInterfaceMixin): 379 | """ 380 | Base class for blocks with packet data. 381 | They must have these fields in their schema: 382 | 383 | * ``packet_len`` is the original amount of data that was "on the wire" 384 | (this can differ from ``captured_len``) 385 | * ``packet_data`` is the actual binary packet data (of course) 386 | 387 | This class makes the ``captured_len`` a read-only property returning 388 | the current length of the packet data. 389 | """ 390 | 391 | __slots__ = [] 392 | readonly_fields = set(("captured_len",)) 393 | 394 | @property 395 | def captured_len(self): 396 | return len(self.packet_data) 397 | 398 | # Helper function. If the user hasn't explicitly set an original packet 399 | # length, use the length of the captured packet data. And if they *have* 400 | # set a length but it's smaller than the captured data, make it the same as 401 | # the captured data length. 402 | @property 403 | def packet_len(self): 404 | plen = self.__getattr__("packet_len") or 0 # this call prevents recursion 405 | return plen or len(self.packet_data) 406 | 407 | 408 | @register_block 409 | class EnhancedPacket(BasePacketBlock, BlockWithTimestampMixin): 410 | """ 411 | "An Enhanced Packet Block (EPB) is the standard container for storing the 412 | packets coming from the network." 413 | - pcapng spec, section 4.3. Other quoted citations are from this section 414 | unless otherwise noted. 415 | """ 416 | 417 | magic_number = 0x00000006 418 | __slots__ = [] 419 | schema = [ 420 | ("interface_id", IntField(32, False), 0), 421 | ("timestamp_high", IntField(32, False), 0), 422 | ("timestamp_low", IntField(32, False), 0), 423 | ("captured_len", IntField(32, False), 0), 424 | ("packet_len", IntField(32, False), 0), 425 | ("packet_data", PacketBytes("captured_len"), b""), 426 | ( 427 | "options", 428 | OptionsField( 429 | [ 430 | Option(2, "epb_flags", "epb_flags"), 431 | Option(3, "epb_hash", "type+bytes", multiple=True), # todo: process 432 | Option(4, "epb_dropcount", "u64"), 433 | Option(5, "epb_packetid", "u64"), 434 | Option(6, "epb_queue", "u32"), 435 | Option( 436 | 7, "epb_verdict", "type+bytes", multiple=True 437 | ), # todo: process 438 | ] 439 | ), 440 | None, 441 | ), 442 | ] 443 | 444 | 445 | @register_block 446 | class SimplePacket(BasePacketBlock): 447 | """ 448 | "The Simple Packet Block (SPB) is a lightweight container for storing the 449 | packets coming from the network." 450 | - pcapng spec, section 4.4. Other quoted citations are from this section 451 | unless otherwise noted. 452 | """ 453 | 454 | magic_number = 0x00000003 455 | __slots__ = [] 456 | schema = [ 457 | # packet_len is NOT the captured length 458 | ("packet_len", IntField(32, False), 0), 459 | # We don't actually use this field; see _decode()/_encode() 460 | ("packet_data", PacketBytes("captured_len"), b""), 461 | ] 462 | 463 | def __init__(self, section, **kwargs): 464 | self.section = section 465 | if "raw" in kwargs: 466 | # Since we can't be sure of the packet length without the snap length, 467 | # we have to do a bit of work here 468 | stream = io.BytesIO(kwargs["raw"]) 469 | self._decoded = struct_decode(self.schema[:1], stream, kwargs["endianness"]) 470 | # Now we can get our ``captured_len`` property which is required 471 | # to really know how much data we can load 472 | rb = RawBytes(self.captured_len) 473 | self._decoded["packet_data"] = rb.load(stream) 474 | else: 475 | super(SimplePacket, self).__init__(section, **kwargs) 476 | 477 | readonly_fields = set(("captured_len", "interface_id")) 478 | 479 | @property 480 | def interface_id(self): 481 | """ 482 | "The Simple Packet Block does not contain the Interface ID field. 483 | Therefore, it MUST be assumed that all the Simple Packet Blocks have 484 | been captured on the interface previously specified in the first 485 | Interface Description Block." 486 | """ 487 | return 0 488 | 489 | @property 490 | def captured_len(self): 491 | """ 492 | "...the SnapLen value MUST be used to determine the size of the Packet 493 | Data field length." 494 | """ 495 | snap_len = self.interface.snaplen 496 | if snap_len == 0: # unlimited 497 | return self.packet_len 498 | else: 499 | return min(snap_len, self.packet_len) 500 | 501 | def _encode(self, outstream): 502 | fld_size = IntField(32, False) 503 | fld_data = RawBytes(0) 504 | if len(self.section.interfaces) > 1: 505 | # Spec is a bit ambiguous here. Section 4.4 says "it MUST 506 | # be assumed that all the Simple Packet Blocks have been captured 507 | # on the interface previously specified in the first Interface 508 | # Description Block." but later adds "A Simple Packet Block cannot 509 | # be present in a Section that has more than one interface because 510 | # of the impossibility to refer to the correct one (it does not 511 | # contain any Interface ID field)." Why would it say "the first" 512 | # IDB and not "the only" IDB if this was really forbidden? 513 | strictness.problem( 514 | "writing SimplePacket for section with multiple interfaces" 515 | ) 516 | if strictness.should_fix(): 517 | # Can't fix this. The IDBs have already been written. 518 | pass 519 | snap_len = self.interface.snaplen 520 | if snap_len > 0 and snap_len < self.captured_len: 521 | # This isn't a strictness issue, it *will* break other readers 522 | # if we write more bytes than the snaplen says to expect. 523 | fld_size.encode( 524 | self.packet_len, outstream, endianness=self.section.endianness 525 | ) 526 | fld_data.encode(self.packet_data[:snap_len], outstream) 527 | else: 528 | fld_size.encode( 529 | self.packet_len, outstream, endianness=self.section.endianness 530 | ) 531 | fld_data.encode(self.packet_data, outstream) 532 | 533 | 534 | @register_block 535 | class ObsoletePacket(BasePacketBlock, BlockWithTimestampMixin): 536 | """ 537 | "The Packet Block is obsolete, and MUST NOT be used in new files. [...] A 538 | Packet Block was a container for storing packets coming from the network." 539 | - pcapng spec, Appendix A. Other quoted citations are from this appendix 540 | unless otherwise noted. 541 | """ 542 | 543 | magic_number = 0x00000002 544 | __slots__ = [] 545 | schema = [ 546 | ("interface_id", IntField(16, False), 0), 547 | ("drops_count", IntField(16, False), 0), 548 | ("timestamp_high", IntField(32, False), 0), 549 | ("timestamp_low", IntField(32, False), 0), 550 | ("captured_len", IntField(32, False), 0), 551 | ("packet_len", IntField(32, False), 0), 552 | ("packet_data", PacketBytes("captured_len"), None), 553 | ( 554 | # The options have the same definitions as their epb_ equivalents 555 | "options", 556 | OptionsField( 557 | [ 558 | Option(2, "pack_flags", "epb_flags"), 559 | Option(3, "pack_hash", "type+bytes", multiple=True), 560 | ] 561 | ), 562 | None, 563 | ), 564 | ] 565 | 566 | def enhanced(self): 567 | """Return an EnhancedPacket with this block's attributes.""" 568 | opts_dict = dict(self.options) 569 | opts_dict["epb_dropcount"] = self.drops_count 570 | for a in ("flags", "hash"): 571 | try: 572 | opts_dict["epb_" + a] = opts_dict.pop("pack_" + a) 573 | except KeyError: 574 | pass 575 | return self.section.new_member( 576 | EnhancedPacket, 577 | interface_id=self.interface_id, 578 | timestamp_high=self.timestamp_high, 579 | timestamp_low=self.timestamp_low, 580 | packet_len=self.packet_len, 581 | packet_data=self.packet_data, 582 | options=opts_dict, 583 | ) 584 | 585 | # Do this check in _write() instead of _encode() to ensure the block gets written 586 | # with the correct magic number. 587 | def _write(self, outstream): 588 | strictness.problem("Packet Block is obsolete and must not be used") 589 | if strictness.should_fix(): 590 | self.enhanced()._write(outstream) 591 | else: 592 | super(ObsoletePacket, self)._write(outstream) 593 | 594 | 595 | @register_block 596 | class NameResolution(SectionMemberBlock): 597 | """ 598 | "The Name Resolution Block (NRB) is used to support the correlation of 599 | numeric addresses (present in the captured packets) and their corresponding 600 | canonical names [...]. Having the literal names saved in the file prevents 601 | the need for performing name resolution at a later time, when the 602 | association between names and addresses may be different from the one in 603 | use at capture time." 604 | - pcapng spec, section 4.5. Other quoted citations are from this section 605 | unless otherwise noted. 606 | """ 607 | 608 | magic_number = 0x00000004 609 | __slots__ = [] 610 | schema = [ 611 | ("records", ListField(NameResolutionRecordField()), []), 612 | ( 613 | "options", 614 | OptionsField( 615 | [ 616 | Option(2, "ns_dnsname", "string"), 617 | Option(3, "ns_dnsIP4addr", "ipv4"), 618 | Option(4, "ns_dnsIP6addr", "ipv6"), 619 | ] 620 | ), 621 | None, 622 | ), 623 | ] 624 | 625 | 626 | @register_block 627 | class InterfaceStatistics( 628 | SectionMemberBlock, BlockWithTimestampMixin, BlockWithInterfaceMixin 629 | ): 630 | """ 631 | "The Interface Statistics Block (ISB) contains the capture statistics for a 632 | given interface [...]. The statistics are referred to the interface defined 633 | in the current Section identified by the Interface ID field." 634 | - pcapng spec, section 4.6. Other quoted citations are from this section 635 | unless otherwise noted. 636 | """ 637 | 638 | magic_number = 0x00000005 639 | __slots__ = [] 640 | schema = [ 641 | ("interface_id", IntField(32, False), 0), 642 | ("timestamp_high", IntField(32, False), 0), 643 | ("timestamp_low", IntField(32, False), 0), 644 | ( 645 | "options", 646 | OptionsField( 647 | [ 648 | Option(2, "isb_starttime", "u64"), # todo: consider resolution 649 | Option(3, "isb_endtime", "u64"), 650 | Option(4, "isb_ifrecv", "u64"), 651 | Option(5, "isb_ifdrop", "u64"), 652 | Option(6, "isb_filteraccept", "u64"), 653 | Option(7, "isb_osdrop", "u64"), 654 | Option(8, "isb_usrdeliv", "u64"), 655 | ] 656 | ), 657 | None, 658 | ), 659 | ] 660 | 661 | 662 | class UnknownBlock(Block): 663 | """ 664 | Class used to represent an unknown block. 665 | 666 | Its block type and raw data will be stored directly with no further 667 | processing. 668 | """ 669 | 670 | __slots__ = ["block_type", "data"] 671 | 672 | def __init__(self, block_type, data): 673 | self.block_type = block_type 674 | self.data = data 675 | 676 | def __repr__(self): 677 | return "UnknownBlock(0x{0:08X}, {1!r})".format(self.block_type, self.data) 678 | -------------------------------------------------------------------------------- /pcapng/constants/__init__.py: -------------------------------------------------------------------------------- 1 | """Generic contsnts""" 2 | 3 | # Byte order magic numbers 4 | # ---------------------------------------- 5 | 6 | ORDER_MAGIC_LE = 0x1A2B3C4D 7 | ORDER_MAGIC_BE = 0x4D3C2B1A 8 | 9 | SIZE_NOTSET = 0xFFFFFFFFFFFFFFFF # 64bit "-1" 10 | 11 | # Endianness constants 12 | 13 | ENDIAN_NATIVE = 0 # '=' 14 | ENDIAN_LITTLE = 1 # '<' 15 | ENDIAN_BIG = 2 # '>' 16 | -------------------------------------------------------------------------------- /pcapng/constants/block_types.py: -------------------------------------------------------------------------------- 1 | # PCAPNG Block types 2 | 3 | BLK_RESERVED = 0x00000000 # Reserved 4 | BLK_INTERFACE = 0x00000001 # Interface description block 5 | BLK_PACKET = 0x00000002 # Packet Block 6 | BLK_PACKET_SIMPLE = 0x00000003 # Simple Packet block 7 | BLK_NAME_RESOLUTION = 0x00000004 # Name Resolution Block 8 | BLK_INTERFACE_STATS = 0x00000005 # Interface Statistics Block 9 | BLK_ENHANCED_PACKET = 0x00000006 # Enhanced Packet Block 10 | 11 | # IRIG Timestamp Block (requested by Gianluca Varenni 12 | # , CACE Technologies LLC) 13 | BLK_IRIG_TIMESTAMP = 0x00000007 14 | 15 | # Arinc 429 in AFDX Encapsulation Information Block 16 | # (requested by Gianluca Varenni , 17 | # CACE Technologies LLC) 18 | BLK_ARINC429 = 0x00000008 19 | 20 | BLK_SECTION_HEADER = 0x0A0D0D0A # Section Header Block 21 | 22 | # Ranges of reserved blocks used to indicate corrupted file. 23 | # Reserved. Used to detect trace files corrupted because 24 | # of file transfers using the HTTP protocol in text mode. 25 | BLK_RESERVED_CORRUPTED = [ 26 | (0x0A0D0A00, 0x0A0D0AFF), 27 | (0x000A0D0A, 0xFF0A0D0A), 28 | (0x000A0D0D, 0xFF0A0D0D), 29 | (0x0D0D0A00, 0x0D0D0AFF), 30 | ] 31 | -------------------------------------------------------------------------------- /pcapng/constants/link_types.py: -------------------------------------------------------------------------------- 1 | # Canonical list maintained at https://www.tcpdump.org/linktypes.html 2 | # 3 | # Some of the reserved values integrated with 4 | # http://www.winpcap.org/ntar/draft/PCAP-DumpFileFormat.html#appendixLinkTypes 5 | 6 | # No link layer information. A packet saved with this link layer 7 | # contains a raw L3 packet preceded by a 32-bit host-byte-order AF_ 8 | # value indicating the specific L3 type. 9 | LINKTYPE_NULL = 0 10 | 11 | # D/I/X and 802.3 Ethernet 12 | LINKTYPE_ETHERNET = 1 13 | 14 | # Experimental Ethernet (3Mb) 15 | LINKTYPE_EXP_ETHERNET = 2 16 | 17 | # Amateur Radio AX.25 18 | LINKTYPE_AX25 = 3 19 | 20 | # Proteon ProNET Token Ring 21 | LINKTYPE_PRONET = 4 22 | 23 | # Chaos 24 | LINKTYPE_CHAOS = 5 25 | 26 | # IEEE 802 Networks 27 | LINKTYPE_TOKEN_RING = 6 28 | 29 | # ARCNET, with BSD-style header 30 | LINKTYPE_ARCNET = 7 31 | 32 | # Serial Line IP 33 | LINKTYPE_SLIP = 8 34 | 35 | # Point-to-point Protocol 36 | LINKTYPE_PPP = 9 37 | 38 | # FDDI 39 | LINKTYPE_FDDI = 10 40 | 41 | # PPP in HDLC-like framing 42 | LINKTYPE_PPP_HDLC = 50 43 | 44 | # NetBSD PPP-over-Ethernet 45 | LINKTYPE_PPP_ETHER = 51 46 | 47 | # Symantec Enterprise Firewall 48 | LINKTYPE_SYMANTEC_FIREWALL = 99 49 | 50 | # LLC/SNAP-encapsulated ATM 51 | LINKTYPE_ATM_RFC1483 = 100 52 | 53 | # Raw IP 54 | LINKTYPE_RAW = 101 55 | 56 | # BSD/OS SLIP BPF header 57 | LINKTYPE_SLIP_BSDOS = 102 58 | 59 | # BSD/OS PPP BPF header 60 | LINKTYPE_PPP_BSDOS = 103 61 | 62 | # Cisco HDLC 63 | LINKTYPE_C_HDLC = 104 64 | 65 | # IEEE 802.11 (wireless) 66 | LINKTYPE_IEEE802_11 = 105 67 | 68 | # Linux Classical IP over ATM 69 | LINKTYPE_ATM_CLIP = 106 70 | 71 | # Frame Relay 72 | LINKTYPE_FRELAY = 107 73 | 74 | # OpenBSD loopback 75 | LINKTYPE_LOOP = 108 76 | 77 | # OpenBSD IPSEC enc 78 | LINKTYPE_ENC = 109 79 | 80 | # ATM LANE + 802.3 (Reserved for future use) 81 | LINKTYPE_LANE8023 = 110 82 | 83 | # NetBSD HIPPI (Reserved for future use) 84 | LINKTYPE_HIPPI = 111 85 | 86 | # NetBSD HDLC framing (Reserved for future use) 87 | LINKTYPE_HDLC = 112 88 | 89 | # Linux cooked socket capture 90 | LINKTYPE_LINUX_SLL = 113 91 | 92 | # Apple LocalTalk hardware 93 | LINKTYPE_LTALK = 114 94 | 95 | # Acorn Econet 96 | LINKTYPE_ECONET = 115 97 | 98 | # Reserved for use with OpenBSD ipfilter 99 | LINKTYPE_IPFILTER = 116 100 | 101 | # OpenBSD DLT_PFLOG 102 | LINKTYPE_PFLOG = 117 103 | 104 | # For Cisco-internal use 105 | LINKTYPE_CISCO_IOS = 118 106 | 107 | # 802.11+Prism II monitor mode 108 | LINKTYPE_PRISM_HEADER = 119 109 | 110 | # FreeBSD Aironet driver stuff 111 | LINKTYPE_AIRONET_HEADER = 120 112 | 113 | # Reserved for Siemens HiPath HDLC 114 | LINKTYPE_HHDLC = 121 115 | 116 | # RFC 2625 IP-over-Fibre Channel 117 | LINKTYPE_IP_OVER_FC = 122 118 | 119 | # Solaris+SunATM 120 | LINKTYPE_SUNATM = 123 121 | 122 | # RapidIO - Reserved as per request from Kent Dahlgren 123 | # for private use. 124 | LINKTYPE_RIO = 124 125 | 126 | # PCI Express - Reserved as per request from Kent Dahlgren 127 | # for private use. 128 | LINKTYPE_PCI_EXP = 125 129 | 130 | # Xilinx Aurora link layer - Reserved as per request from Kent 131 | # Dahlgren for private use. 132 | LINKTYPE_AURORA = 126 133 | 134 | # 802.11 plus BSD radio header 135 | LINKTYPE_IEEE802_11_RADIO = 127 136 | 137 | # Tazmen Sniffer Protocol - Reserved for the TZSP encapsulation, as 138 | # per request from Chris Waters 139 | # TZSP is a generic encapsulation for any other link type, which 140 | # includes a means to include meta-information with the packet, 141 | # e.g. signal strength and channel for 802.11 packets. 142 | LINKTYPE_TZSP = 128 143 | 144 | # Linux-style headers 145 | LINKTYPE_ARCNET_LINUX = 129 146 | 147 | # Juniper-private data link type, as per request from Hannes Gredler 148 | # . The corresponding DLT_s are used for passing 149 | # on chassis-internal metainformation such as QOS profiles, etc.. 150 | LINKTYPE_JUNIPER_MLPPP = 130 151 | 152 | # Juniper-private data link type, as per request from Hannes Gredler 153 | # . The corresponding DLT_s are used for passing 154 | # on chassis-internal metainformation such as QOS profiles, etc.. 155 | LINKTYPE_JUNIPER_MLFR = 131 156 | 157 | # Juniper-private data link type, as per request from Hannes Gredler 158 | # . The corresponding DLT_s are used for passing 159 | # on chassis-internal metainformation such as QOS profiles, etc.. 160 | LINKTYPE_JUNIPER_ES = 132 161 | 162 | # Juniper-private data link type, as per request from Hannes Gredler 163 | # . The corresponding DLT_s are used for passing 164 | # on chassis-internal metainformation such as QOS profiles, etc.. 165 | LINKTYPE_JUNIPER_GGSN = 133 166 | 167 | # Juniper-private data link type, as per request from Hannes Gredler 168 | # . The corresponding DLT_s are used for passing 169 | # on chassis-internal metainformation such as QOS profiles, etc.. 170 | LINKTYPE_JUNIPER_MFR = 134 171 | 172 | # Juniper-private data link type, as per request from Hannes Gredler 173 | # . The corresponding DLT_s are used for passing 174 | # on chassis-internal metainformation such as QOS profiles, etc.. 175 | LINKTYPE_JUNIPER_ATM2 = 135 176 | 177 | # Juniper-private data link type, as per request from Hannes Gredler 178 | # . The corresponding DLT_s are used for passing 179 | # on chassis-internal metainformation such as QOS profiles, etc.. 180 | LINKTYPE_JUNIPER_SERVICES = 136 181 | 182 | # Juniper-private data link type, as per request from Hannes Gredler 183 | # . The corresponding DLT_s are used for passing 184 | # on chassis-internal metainformation such as QOS profiles, etc.. 185 | LINKTYPE_JUNIPER_ATM1 = 137 186 | 187 | # Apple IP-over-IEEE 1394 cooked header 188 | LINKTYPE_APPLE_IP_OVER_IEEE1394 = 138 189 | 190 | # ??? 191 | LINKTYPE_MTP2_WITH_PHDR = 139 192 | 193 | # ??? 194 | LINKTYPE_MTP2 = 140 195 | 196 | # ??? 197 | LINKTYPE_MTP3 = 141 198 | 199 | # ??? 200 | LINKTYPE_SCCP = 142 201 | 202 | # DOCSIS MAC frames 203 | LINKTYPE_DOCSIS = 143 204 | 205 | # Linux-IrDA 206 | LINKTYPE_LINUX_IRDA = 144 207 | 208 | # Reserved for IBM SP switch and IBM Next Federation switch. 209 | LINKTYPE_IBM_SP = 145 210 | 211 | # Reserved for IBM SP switch and IBM Next Federation switch. 212 | LINKTYPE_IBM_SN = 146 213 | 214 | # USB packets, beginning with a Linux USB header, as specified by the 215 | # struct usbmon_packet in the Documentation/usb/usbmon.txt file in the 216 | # Linux source tree. Only the first 48 bytes of that header are present. 217 | LINKTYPE_USB_LINUX = 189 218 | 219 | # USB packets, beginning with a Linux USB header, as specified by the 220 | # struct usbmon_packet in the Documentation/usb/usbmon.txt file in the 221 | # Linux source tree. All 64 bytes of the header are present. 222 | LINKTYPE_USB_LINUX_MMAPPED = 220 223 | 224 | # DOCSIS with Excentis XRA pseudo-header 225 | LINKTYPE_DOCSIS31_XRA31 = 273 226 | 227 | 228 | LINKTYPE_DESCRIPTIONS = { 229 | LINKTYPE_NULL: "No link layer information.", 230 | LINKTYPE_ETHERNET: "D/I/X and 802.3 Ethernet", 231 | LINKTYPE_EXP_ETHERNET: "Experimental Ethernet (3Mb)", 232 | LINKTYPE_AX25: "Amateur Radio AX.25", 233 | LINKTYPE_PRONET: "Proteon ProNET Token Ring", 234 | LINKTYPE_CHAOS: "Chaos", 235 | LINKTYPE_TOKEN_RING: "IEEE 802 Networks", 236 | LINKTYPE_ARCNET: "ARCNET, with BSD-style header", 237 | LINKTYPE_SLIP: "Serial Line IP", 238 | LINKTYPE_PPP: "Point-to-point Protocol", 239 | LINKTYPE_FDDI: "FDDI", 240 | LINKTYPE_PPP_HDLC: "PPP in HDLC-like framing", 241 | LINKTYPE_PPP_ETHER: "NetBSD PPP-over-Ethernet", 242 | LINKTYPE_SYMANTEC_FIREWALL: "Symantec Enterprise Firewall", 243 | LINKTYPE_ATM_RFC1483: "LLC/SNAP-encapsulated ATM", 244 | LINKTYPE_RAW: "Raw IP", 245 | LINKTYPE_SLIP_BSDOS: "BSD/OS SLIP BPF header", 246 | LINKTYPE_PPP_BSDOS: "BSD/OS PPP BPF header", 247 | LINKTYPE_C_HDLC: "Cisco HDLC", 248 | LINKTYPE_IEEE802_11: "IEEE 802.11 (wireless)", 249 | LINKTYPE_ATM_CLIP: "Linux Classical IP over ATM", 250 | LINKTYPE_FRELAY: "Frame Relay", 251 | LINKTYPE_LOOP: "OpenBSD loopback", 252 | LINKTYPE_ENC: "OpenBSD IPSEC enc", 253 | LINKTYPE_LANE8023: "ATM LANE + 802.3 (Reserved for future use)", 254 | LINKTYPE_HIPPI: "NetBSD HIPPI (Reserved for future use)", 255 | LINKTYPE_HDLC: "NetBSD HDLC framing (Reserved for future use)", 256 | LINKTYPE_LINUX_SLL: "Linux cooked socket capture", 257 | LINKTYPE_LTALK: "Apple LocalTalk hardware", 258 | LINKTYPE_ECONET: "Acorn Econet", 259 | LINKTYPE_IPFILTER: "Reserved for use with OpenBSD ipfilter", 260 | LINKTYPE_PFLOG: "OpenBSD DLT_PFLOG", 261 | LINKTYPE_CISCO_IOS: "For Cisco-internal use", 262 | LINKTYPE_PRISM_HEADER: "802.11+Prism II monitor mode", 263 | LINKTYPE_AIRONET_HEADER: "FreeBSD Aironet driver stuff", 264 | LINKTYPE_HHDLC: "Reserved for Siemens HiPath HDLC", 265 | LINKTYPE_IP_OVER_FC: "RFC 2625 IP-over-Fibre Channel", 266 | LINKTYPE_SUNATM: "Solaris+SunATM", 267 | LINKTYPE_RIO: "RapidIO (private use)", 268 | LINKTYPE_PCI_EXP: "PCI Express (private use)", 269 | LINKTYPE_AURORA: "Xilinx Aurora link layer (private use)", 270 | LINKTYPE_IEEE802_11_RADIO: "802.11 plus BSD radio header", 271 | LINKTYPE_TZSP: "Tazmen Sniffer Protocol", 272 | LINKTYPE_ARCNET_LINUX: "Linux-style headers", 273 | LINKTYPE_JUNIPER_MLPPP: "Juniper-private data link type", 274 | LINKTYPE_JUNIPER_MLFR: "Juniper-private data link type", 275 | LINKTYPE_JUNIPER_ES: "Juniper-private data link type", 276 | LINKTYPE_JUNIPER_GGSN: "Juniper-private data link type", 277 | LINKTYPE_JUNIPER_MFR: "Juniper-private data link type", 278 | LINKTYPE_JUNIPER_ATM2: "Juniper-private data link type", 279 | LINKTYPE_JUNIPER_SERVICES: "Juniper-private data link type", 280 | LINKTYPE_JUNIPER_ATM1: "Juniper-private data link type", 281 | LINKTYPE_APPLE_IP_OVER_IEEE1394: "Apple IP-over-IEEE 1394 cooked header", 282 | # LINKTYPE_MTP2_WITH_PHDR: '???', 283 | # LINKTYPE_MTP2: '???', 284 | # LINKTYPE_MTP3: '???', 285 | # LINKTYPE_SCCP: '???', 286 | LINKTYPE_DOCSIS: "DOCSIS MAC frames", 287 | LINKTYPE_LINUX_IRDA: "Linux-IrDA", 288 | LINKTYPE_IBM_SP: "Reserved for IBM SP switch and IBM Next Federation switch.", # noqa 289 | LINKTYPE_IBM_SN: "Reserved for IBM SP switch and IBM Next Federation switch.", # noqa 290 | LINKTYPE_USB_LINUX: "USB packets, beginning with a 48-bytes Linux USB header.", # noqa 291 | LINKTYPE_USB_LINUX_MMAPPED: "USB packets, beginning with a 64-bytes Linux USB header.", # noqa 292 | LINKTYPE_DOCSIS31_XRA31: "DOCSIS with Excentis XRA pseudo-header", 293 | } 294 | -------------------------------------------------------------------------------- /pcapng/exceptions.py: -------------------------------------------------------------------------------- 1 | class PcapngException(Exception): 2 | """Base for all the pcapng exceptions""" 3 | 4 | pass 5 | 6 | 7 | class PcapngWarning(Warning): 8 | """Base for all the pcapng warnings""" 9 | 10 | pass 11 | 12 | 13 | class PcapngLoadError(PcapngException): 14 | """Indicate an error while loading a pcapng file""" 15 | 16 | pass 17 | 18 | 19 | class PcapngDumpError(PcapngException): 20 | """Indicate an error while writing a pcapng file""" 21 | 22 | pass 23 | 24 | 25 | class PcapngStrictnessError(PcapngException): 26 | """Indicate a condition about poorly formed pcapng files""" 27 | 28 | 29 | class PcapngStrictnessWarning(PcapngWarning): 30 | """Indicate a condition about poorly formed pcapng files""" 31 | 32 | 33 | class StreamEmpty(PcapngLoadError): # End of stream 34 | """ 35 | Exception indicating that the end of the stream was reached 36 | and exactly zero bytes were read; usually it simply indicates 37 | we reached the end of the stream and no further content is 38 | available for reading. 39 | """ 40 | 41 | pass 42 | 43 | 44 | class CorruptedFile(PcapngLoadError): 45 | """ 46 | Exception used to indicate that something is wrong with the 47 | file structure, possibly due to data corruption. 48 | """ 49 | 50 | pass 51 | 52 | 53 | class TruncatedFile(PcapngLoadError): 54 | """ 55 | Exception used to indicate that not all the required bytes 56 | could be read before stream end, but the read length was 57 | non-zero, indicating a possibly truncated stream. 58 | """ 59 | 60 | pass 61 | 62 | 63 | class BadMagic(PcapngLoadError): 64 | """ 65 | Exception used to indicate a failure due to some bad magic 66 | number encountered (either the file magic or section header 67 | byte order marker). 68 | """ 69 | 70 | pass 71 | -------------------------------------------------------------------------------- /pcapng/flags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to wrap an integer in bitwise flag/field accessors. 3 | """ 4 | 5 | from collections import OrderedDict 6 | from collections.abc import Iterable 7 | 8 | from pcapng._compat import namedtuple 9 | 10 | 11 | class FlagBase(object): 12 | """\ 13 | Base class for flag types to be used in a Flags object. 14 | Handles the bitwise math so subclasses don't have to worry about it. 15 | """ 16 | 17 | __slots__ = [ 18 | "owner", 19 | "offset", 20 | "size", 21 | "extra", 22 | "mask", 23 | ] 24 | 25 | def __init__(self, owner, offset, size, extra=None): 26 | if size < 1: 27 | raise TypeError("Flag must be at least 1 bit wide") 28 | if size > owner._nbits: 29 | raise TypeError("Flag must fit into owner size") 30 | self.owner = owner 31 | self.offset = offset 32 | self.size = size 33 | self.extra = extra 34 | self.mask = ((1 << self.size) - 1) << self.offset 35 | 36 | def get_bits(self): 37 | return (self.owner._value & self.mask) >> self.offset 38 | 39 | def set_bits(self, val): 40 | val &= (1 << self.size) - 1 41 | self.owner._value &= ~self.mask 42 | self.owner._value |= val << self.offset 43 | 44 | 45 | class FlagBool(FlagBase): 46 | """Object representing a single boolean flag""" 47 | 48 | def __init__(self, owner, offset, size, extra=None): 49 | if size != 1: 50 | raise TypeError( 51 | "{cls} can only be 1 bit in size".format(cls=self.__class__.__name__) 52 | ) 53 | super(FlagBool, self).__init__(owner, offset, size) 54 | 55 | def get(self): 56 | return bool(self.get_bits()) 57 | 58 | def set(self, val): 59 | self.set_bits(int(bool(val))) 60 | 61 | 62 | class FlagUInt(FlagBase): 63 | """\ 64 | Object representing an unsigned integer of the given size stored in 65 | a larger bitfield 66 | """ 67 | 68 | def get(self): 69 | return self.get_bits() 70 | 71 | def set(self, val): 72 | self.set_bits(val) 73 | 74 | 75 | class FlagEnum(FlagBase): 76 | """\ 77 | Object representing a range of values stored in part of a larger 78 | bitfield 79 | """ 80 | 81 | def __init__(self, owner, offset, size, extra=None): 82 | if not isinstance(extra, Iterable): 83 | raise TypeError( 84 | "{cls} needs an iterable of values".format(cls=self.__class__.__name__) 85 | ) 86 | extra = list(extra) 87 | if len(extra) > 2**size: 88 | raise TypeError( 89 | "{cls} iterable has too many values (got {got}, " 90 | "{size} bits only address {max})".format( 91 | cls=self.__class__.__name__, 92 | got=len(extra), 93 | size=size, 94 | max=2**size, 95 | ) 96 | ) 97 | 98 | super(FlagEnum, self).__init__(owner, offset, size, extra) 99 | 100 | def get(self): 101 | val = self.get_bits() 102 | try: 103 | return self.extra[val] 104 | except IndexError: 105 | return "[invalid value]" 106 | 107 | def set(self, val): 108 | if val in self.extra: 109 | self.set_bits(self.extra.index(val)) 110 | elif isinstance(val, int): 111 | self.set_bits(val) 112 | else: 113 | raise TypeError( 114 | "Invalid value {val} for {cls}".format( 115 | val=val, cls=self.__class__.__name__ 116 | ) 117 | ) 118 | 119 | 120 | # Class representing a single flag schema for FlagWord. 121 | # 'nbits' defaults to 1, and 'extra' defaults to None. 122 | FlagField = namedtuple( 123 | "FlagField", ("name", "ftype", "nbits", "extra"), defaults=(1, None) 124 | ) 125 | 126 | 127 | class FlagWord(object): 128 | """\ 129 | Class to wrap an integer in bitwise flag/field accessors. 130 | """ 131 | 132 | __slots__ = [ 133 | "_nbits", 134 | "_value", 135 | "_schema", 136 | ] 137 | 138 | def __init__(self, schema, nbits=32, initial=0): 139 | """ 140 | :param schema: 141 | A list of FlagField objects representing the values to be packed 142 | into this object, in order from LSB to MSB of the underlying int 143 | 144 | :param nbits: 145 | An integer representing the total number of bits used for flags 146 | 147 | :param initial: 148 | The initial integer value of the flags field 149 | """ 150 | 151 | self._nbits = nbits 152 | self._value = initial 153 | self._schema = OrderedDict() 154 | 155 | tot_bits = sum([item.nbits for item in schema]) 156 | if tot_bits > nbits: 157 | raise TypeError( 158 | "Too many fields for {nbits}-bit field " 159 | "(schema defines {tot} bits)".format(nbits=nbits, tot=tot_bits) 160 | ) 161 | 162 | bitn = 0 163 | for item in schema: 164 | if not isinstance(item, FlagField): 165 | raise TypeError("Schema must be composed of FlagField objects") 166 | if not issubclass(item.ftype, FlagBase): 167 | raise TypeError("Expected FlagBase, got {}".format(item.ftype)) 168 | self._schema[item.name] = item.ftype(self, bitn, item.nbits, item.extra) 169 | bitn += item.nbits 170 | 171 | def __int__(self): 172 | return self._value 173 | 174 | def __repr__(self): 175 | rv = "<{0} (value={1})".format(self.__class__.__name__, self._value) 176 | for k, v in self._schema.items(): 177 | rv += " {0}={1}".format(k, v.get()) 178 | return rv + ">" 179 | 180 | def __getattr__(self, name): 181 | try: 182 | v = self._schema[name] 183 | except KeyError: 184 | raise AttributeError(name) 185 | return v.get() 186 | 187 | def __setattr__(self, name, val): 188 | try: 189 | return object.__setattr__(self, name, val) 190 | except AttributeError: 191 | pass 192 | try: 193 | v = self._schema[name] 194 | except KeyError: 195 | raise AttributeError(name) 196 | return v.set(val) 197 | -------------------------------------------------------------------------------- /pcapng/scanner.py: -------------------------------------------------------------------------------- 1 | import pcapng.blocks as blocks 2 | from pcapng.constants.block_types import BLK_RESERVED, BLK_RESERVED_CORRUPTED 3 | from pcapng.exceptions import CorruptedFile, StreamEmpty 4 | from pcapng.structs import ( 5 | SECTION_HEADER_MAGIC, 6 | read_block_data, 7 | read_int, 8 | read_section_header, 9 | ) 10 | 11 | 12 | class FileScanner(object): 13 | """ 14 | pcap-ng file scanner. 15 | 16 | This object can be iterated to get blocks out of a pcap-ng 17 | stream (a file or file-like object providing a .read() method). 18 | 19 | Example usage: 20 | 21 | .. code-block:: python 22 | 23 | from pcapng import FileScanner 24 | 25 | with open('/tmp/mycapture.pcap', 'rb') as fp: 26 | scanner = FileScanner(fp) 27 | for block in scanner: 28 | pass # do something with the block... 29 | 30 | :param stream: 31 | a file-like object from which to read the data. 32 | If you need to parse data from some string you have entirely in-memory, 33 | just wrap it in a :py:class:`io.BytesIO` object. 34 | """ 35 | 36 | __slots__ = ["stream", "current_section", "endianness"] 37 | 38 | def __init__(self, stream): 39 | self.stream = stream 40 | self.current_section = None 41 | self.endianness = "=" 42 | 43 | def __iter__(self): 44 | while True: 45 | try: 46 | yield self._read_next_block() 47 | except StreamEmpty: 48 | return 49 | 50 | def _read_next_block(self): 51 | block_type = self._read_int(32, False) 52 | 53 | if block_type == SECTION_HEADER_MAGIC: 54 | block = self._read_section_header() 55 | self.current_section = block 56 | self.endianness = block.endianness 57 | return block 58 | 59 | if self.current_section is None: 60 | raise ValueError("File not starting with a proper section header") 61 | 62 | block = self._read_block(block_type) 63 | 64 | return block 65 | 66 | def _read_section_header(self): 67 | """ 68 | Section information headers are special blocks in that they 69 | modify the state of the FileScanner instance (to change current 70 | section / endianness) 71 | """ 72 | 73 | section_info = read_section_header(self.stream) 74 | self.endianness = section_info["endianness"] # todo: use property? 75 | 76 | # todo: make this use the standard schema facilities as well! 77 | return blocks.SectionHeader( 78 | raw=section_info["data"], endianness=section_info["endianness"] 79 | ) 80 | 81 | def _read_block(self, block_type): 82 | """ 83 | Read the block payload and pass to the appropriate block constructor 84 | """ 85 | data = read_block_data(self.stream, endianness=self.endianness) 86 | 87 | if block_type in blocks.KNOWN_BLOCKS: 88 | # This is a known block -- instantiate it 89 | return self.current_section.new_member( 90 | blocks.KNOWN_BLOCKS[block_type], raw=data 91 | ) 92 | 93 | if block_type in BLK_RESERVED_CORRUPTED: 94 | raise CorruptedFile( 95 | "Block type 0x{0:08X} is reserved to detect a corrupted file".format( 96 | block_type 97 | ) 98 | ) 99 | 100 | if block_type == BLK_RESERVED: 101 | raise CorruptedFile( 102 | "Block type 0x00000000 is reserved and should not be used " 103 | "in capture files!" 104 | ) 105 | 106 | return blocks.UnknownBlock(block_type, data) 107 | 108 | def _read_int(self, size, signed=False): 109 | """ 110 | Read an integer from the stream, using current endianness 111 | """ 112 | return read_int(self.stream, size, signed=signed, endianness=self.endianness) 113 | -------------------------------------------------------------------------------- /pcapng/strictness.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for alerting the user when attempting to do things with pcapng that 3 | aren't strictly valid. 4 | """ 5 | 6 | import warnings 7 | from enum import Enum 8 | 9 | from pcapng.exceptions import PcapngStrictnessError, PcapngStrictnessWarning 10 | 11 | 12 | class Strictness(Enum): 13 | NONE = 0 # No warnings, do what you want 14 | WARN = 1 # Do what you want, but warn of potential issues 15 | FIX = 2 # Warn of potential issues, fix *if possible* 16 | FORBID = 3 # raise exception on potential issues 17 | 18 | 19 | strict_level = Strictness.FORBID 20 | 21 | 22 | def set_strictness(level): 23 | assert type(level) is Strictness 24 | global strict_level 25 | strict_level = level 26 | 27 | 28 | def problem(msg): 29 | "Warn or raise an exception with the given message." 30 | if strict_level == Strictness.FORBID: 31 | raise PcapngStrictnessError(msg) 32 | elif strict_level in (Strictness.WARN, Strictness.FIX): 33 | warnings.warn(PcapngStrictnessWarning(msg)) 34 | 35 | 36 | def warn(msg): 37 | "Show a warning with the given message." 38 | if strict_level > Strictness.NONE: 39 | warnings.warn(PcapngStrictnessWarning(msg)) 40 | 41 | 42 | def should_fix(): 43 | "Helper function for showing code used to fix questionable pcapng data." 44 | return strict_level == Strictness.FIX 45 | -------------------------------------------------------------------------------- /pcapng/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | 4 | 5 | def pack_ipv4(data): 6 | return socket.inet_aton(data) 7 | 8 | 9 | def unpack_ipv4(data): 10 | return socket.inet_ntoa(data) 11 | 12 | 13 | def _get_pairs(data): 14 | """Return data in pairs 15 | 16 | This uses a clever hack, based on the fact that zip will consume 17 | items from the same iterator, for each reference found in each 18 | row. 19 | 20 | Example:: 21 | 22 | >>> _get_pairs([1, 2, 3, 4]) 23 | [(1, 2), (3, 4)] 24 | 25 | """ 26 | return list(zip(*((iter(data),) * 2))) 27 | 28 | 29 | def pack_ipv6(data): 30 | return socket.inet_pton(socket.AF_INET6, data) 31 | 32 | 33 | def unpack_ipv6(data): 34 | return socket.inet_ntop(socket.AF_INET6, data) 35 | 36 | 37 | def pack_macaddr(data): 38 | a = [int(x, 16) for x in data.split(":")] 39 | return struct.pack("!6B", *a) 40 | 41 | 42 | def unpack_macaddr(data): 43 | return ":".join(format(x, "02x") for x in data) 44 | 45 | 46 | def pack_euiaddr(data): 47 | a = [int(x, 16) for x in data.split(":")] 48 | return struct.pack("!8B", *a) 49 | 50 | 51 | def unpack_euiaddr(data): 52 | return unpack_macaddr(data) 53 | 54 | 55 | def unpack_timestamp_resolution(data): 56 | """ 57 | Unpack a timestamp resolution. 58 | 59 | Returns a floating point number representing the timestamp 60 | resolution (multiplier). 61 | """ 62 | if len(data) != 1: 63 | raise ValueError("Data must be exactly one byte") 64 | num = data[0] 65 | base = 2 if (num >> 7 & 1) else 10 66 | exponent = num & 0b01111111 67 | return base ** (-exponent) 68 | 69 | 70 | def pack_timestamp_resolution(base, exponent): 71 | """ 72 | Pack a timestamp resolution. 73 | 74 | :param base: 2 or 10 75 | :param exponent: negative power of the base to be encoded 76 | """ 77 | exponent = abs(exponent) 78 | if base == 2: 79 | return struct.pack("B", exponent | 0b10000000) 80 | if base == 10: 81 | return struct.pack("B", exponent) 82 | raise ValueError("Supported bases are: 2, 10") 83 | -------------------------------------------------------------------------------- /pcapng/writer.py: -------------------------------------------------------------------------------- 1 | import pcapng.blocks as blocks 2 | from pcapng.exceptions import PcapngDumpError 3 | 4 | 5 | class FileWriter(object): 6 | """ 7 | pcap-ng file writer. 8 | """ 9 | 10 | __slots__ = [ 11 | "stream", 12 | "interfaces", 13 | "current_section", 14 | ] 15 | 16 | def __init__(self, stream, shb): 17 | """ 18 | Start writing a new pcap-ng section to the given stream. Writes the 19 | :py:class:`SectionHeader` immediately. Also writes any 20 | :py:class:`InterfaceDescription` blocks that have been created for 21 | the section. 22 | 23 | :param stream: 24 | a file-like object to which to write the data. 25 | 26 | :param shb: 27 | a :py:class:`pcapng.blocks.SectionHeader` to start the section. 28 | """ 29 | self.stream = stream 30 | self.interfaces = set() 31 | if not isinstance(shb, blocks.SectionHeader): 32 | raise TypeError("not a SectionHeader") 33 | self.current_section = shb 34 | shb._write(self.stream) 35 | for iface in sorted(shb.interfaces.keys()): 36 | self.interfaces.add(iface) 37 | shb.interfaces[iface]._write(stream) 38 | 39 | def write_block(self, blk): 40 | """ 41 | Write the given block to this stream. 42 | 43 | If the block is a :py:class:`pcapng.blocks.SectionHeader`, then a new 44 | section will be started in the same output stream, along with any 45 | :py:class:`InterfaceDescription` blocks that have been created for the section. 46 | 47 | :param blk: 48 | a :py:class:`pcapng.blocks.Block` to write. 49 | """ 50 | if not isinstance(blk, blocks.Block): 51 | raise TypeError("not a pcapng block") 52 | 53 | if type(blk) is blocks.SectionHeader: 54 | # Starting a new section, so re-initialize 55 | self.__init__(self.stream, blk) 56 | return 57 | 58 | if blk.section is not self.current_section: 59 | raise PcapngDumpError("block not from current section") 60 | 61 | if type(blk) is blocks.InterfaceDescription: 62 | # Have we already written this interface? 63 | if blk.interface_id in self.interfaces: 64 | # We have. Should this be an error? 65 | raise PcapngDumpError( 66 | "duplicate interface_id {}".format(blk.interface_id) 67 | ) 68 | # No, so add it 69 | self.interfaces.add(blk.interface_id) 70 | elif isinstance(blk, blocks.BlockWithInterfaceMixin): 71 | # Check that we've written the interface for this block 72 | if blk.interface.interface_id not in self.interfaces: 73 | raise PcapngDumpError("no matching interface written for block") 74 | 75 | blk._write(self.stream) 76 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | 'setuptools >= 42', 4 | 'wheel', 5 | 'setuptools_scm[toml]>=3.4', 6 | ] 7 | 8 | [tool.setuptools_scm] 9 | 10 | [tool.black] 11 | target-version = ['py35'] 12 | 13 | [tool.isort] 14 | # These settings ensure that black and isort don't disagree on the imports. 15 | line_length = 88 16 | multi_line_output = 3 17 | include_trailing_comma = true 18 | 19 | known_first_party = ['pcapng'] 20 | known_third_party = ['pytest', 'scapy'] 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = python-pcapng 3 | description = Library to read/write the pcap-ng format used by various packet sniffers. 4 | long_description = file: README.rst 5 | long_description_content_type = text/x-rst 6 | url = https://github.com/rshk/python-pcapng 7 | author = Samuele Santi 8 | author_email = samuele@samuelesanti.com 9 | license = Apache-2.0 10 | license_file = LICENSE 11 | license_files = 12 | LICENSE 13 | classifiers = 14 | Development Status :: 5 - Production/Stable 15 | License :: OSI Approved :: Apache Software License 16 | Programming Language :: Python :: 3.5 17 | Programming Language :: Python :: 3.6 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: Implementation :: CPython 21 | 22 | [options] 23 | packages = find: 24 | python_requires = ~= 3.5 25 | zip_safe = False 26 | 27 | [options.extras_require] 28 | dev = 29 | flake8 30 | isort 31 | pre-commit 32 | pytest>=5.4 33 | pytest-cov 34 | setuptools_scm[toml] 35 | sphinx 36 | sphinx-rtd-theme 37 | 38 | [flake8] 39 | max-line-length = 88 40 | 41 | [pycodestyle] 42 | max-line-length = 88 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools_scm # noqa: F401 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /test_data/test001.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test001.ntar -------------------------------------------------------------------------------- /test_data/test002.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test002.ntar -------------------------------------------------------------------------------- /test_data/test003.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test003.ntar -------------------------------------------------------------------------------- /test_data/test004.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test004.ntar -------------------------------------------------------------------------------- /test_data/test005.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test005.ntar -------------------------------------------------------------------------------- /test_data/test006-fixed.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test006-fixed.ntar -------------------------------------------------------------------------------- /test_data/test006.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test006.ntar -------------------------------------------------------------------------------- /test_data/test007.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test007.ntar -------------------------------------------------------------------------------- /test_data/test008.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test008.ntar -------------------------------------------------------------------------------- /test_data/test009.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test009.ntar -------------------------------------------------------------------------------- /test_data/test010.ntar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rshk/python-pcapng/33e722f6d5cc41154fda56d8dff62e2970078fd5/test_data/test010.ntar -------------------------------------------------------------------------------- /tests/test_parse_enhanced_packet.py: -------------------------------------------------------------------------------- 1 | import io 2 | import struct 3 | 4 | import pytest 5 | 6 | from pcapng.blocks import EnhancedPacket, InterfaceDescription, SectionHeader 7 | from pcapng.scanner import FileScanner 8 | from pcapng.utils import pack_timestamp_resolution 9 | 10 | 11 | def test_read_block_enhanced_packet_bigendian(): 12 | scanner = FileScanner( 13 | io.BytesIO( 14 | # ---------- Section header 15 | b"\x0a\x0d\x0d\x0a" # Magic number 16 | b"\x00\x00\x00\x20" # Block size (32 bytes) 17 | b"\x1a\x2b\x3c\x4d" # Magic number 18 | b"\x00\x01\x00\x00" # Version 19 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 20 | b"\x00\x00\x00\x00" # Empty options 21 | b"\x00\x00\x00\x20" # Block size (32 bytes) 22 | # ---------- Interface description 23 | b"\x00\x00\x00\x01" # block magic 24 | b"\x00\x00\x00\x40" # block syze (64 bytes) 25 | b"\x00\x01" # link type 26 | b"\x00\x00" # reserved block 27 | b"\x00\x00\xff\xff" # size limit 28 | b"\x00\x02\x00\x04" 29 | b"eth0" # if_name 30 | b"\x00\x09\x00\x01" 31 | b"\x06\x00\x00\x00" # if_tsresol (+padding) 32 | b"\x00\x0c\x00\x13" 33 | b"Linux 3.2.0-4-amd64\x00" # if_os 34 | b"\x00\x00\x00\x00" # end of options 35 | b"\x00\x00\x00\x40" # block syze (64 bytes) 36 | # ---------- Enhanced packet 37 | b"\x00\x00\x00\x06" # block magic 38 | b"\x00\x00\x00\x78" # block syze (120 bytes) 39 | b"\x00\x00\x00\x00" # interface id (first one, eth0) 40 | b"\x00\x04\xf8\x1e" 41 | b"\x3c\x3e\xd5\xa9" # timestamp (microseconds) 42 | b"\x00\x00\x00\x51" # Captured length 43 | b"\x00\x00\x00\x51" # Original length 44 | # Packet data (81 bytes) 45 | b"\x00\x02\x157\xa2D\x00\xae\xf3R\xaa\xd1\x08\x00" # Ethernet 46 | b"E\x00\x00C\x00\x01\x00\x00@\x06x<\xc0\xa8\x05\x15B#\xfa\x97" # IP 47 | b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 " # TCP 48 | b"\x00\xbb9\x00\x00" # TCP(cont) 49 | b"GET /index.html HTTP/1.0 \n\n" # HTTP 50 | b"\x00\x00\x00" # Padding 51 | # todo: add options? 52 | b"\x00\x00\x00\x00" # Empty options 53 | b"\x00\x00\x00\x78" # block syze (120 bytes) 54 | ) 55 | ) 56 | 57 | blocks = list(scanner) 58 | assert len(blocks) == 3 59 | 60 | assert isinstance(blocks[0], SectionHeader) 61 | assert blocks[0].endianness == ">" 62 | assert blocks[0].interfaces == {0: blocks[1]} 63 | 64 | assert isinstance(blocks[1], InterfaceDescription) 65 | assert blocks[1].section == blocks[0] 66 | assert blocks[1].link_type == 0x01 67 | assert blocks[1].snaplen == 0xFFFF 68 | assert blocks[1].options["if_name"] == "eth0" 69 | assert blocks[1].options["if_tsresol"] == b"\x06" 70 | 71 | assert isinstance(blocks[2], EnhancedPacket) 72 | assert blocks[2].section == blocks[0] 73 | assert blocks[2].interface_id == 0 74 | assert blocks[2].interface == blocks[1] 75 | 76 | assert blocks[2].timestamp_high == 0x0004F81E 77 | assert blocks[2].timestamp_low == 0x3C3ED5A9 78 | assert blocks[2].timestamp_resolution == 1e-6 79 | assert blocks[2].timestamp == 1398708650.3008409 80 | 81 | assert blocks[2].captured_len == 0x51 82 | assert blocks[2].packet_len == 0x51 83 | assert blocks[2].packet_data == ( 84 | b"\x00\x02\x157\xa2D\x00\xae\xf3R\xaa\xd1\x08\x00" # Ethernet 85 | b"E\x00\x00C\x00\x01\x00\x00@\x06x<\xc0\xa8\x05\x15B#\xfa\x97" # IP 86 | b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 " # TCP 87 | b"\x00\xbb9\x00\x00" # TCP(cont) 88 | b"GET /index.html HTTP/1.0 \n\n" 89 | ) # HTTP 90 | assert len(blocks[2].options) == 0 91 | 92 | 93 | def _generate_file_with_tsresol(base, exponent): 94 | tsresol = pack_timestamp_resolution(base, exponent) 95 | base_timestamp = 1420070400.0 # 2015-01-01 00:00 UTC 96 | timestamp = base_timestamp / (base**exponent) 97 | 98 | stream = io.BytesIO() 99 | 100 | # ---------- Section header 101 | stream.write(b"\x0a\x0d\x0d\x0a") # Magic number 102 | stream.write(b"\x00\x00\x00\x20") # Block size (32 bytes) 103 | stream.write(b"\x1a\x2b\x3c\x4d") # Magic number 104 | stream.write(b"\x00\x01\x00\x00") # Version 105 | stream.write(b"\xff\xff\xff\xff\xff\xff\xff\xff") # Undefined length 106 | stream.write(b"\x00\x00\x00\x00") # Empty options 107 | stream.write(b"\x00\x00\x00\x20") # Block size (32 bytes) 108 | 109 | # ---------- Interface description 110 | stream.write(b"\x00\x00\x00\x01") # block magic 111 | stream.write(b"\x00\x00\x00\x20") # block syze 112 | stream.write(b"\x00\x01") # link type 113 | stream.write(b"\x00\x00") # reserved block 114 | stream.write(b"\x00\x00\xff\xff") # size limit 115 | stream.write(b"\x00\x09\x00\x01") 116 | stream.write(tsresol) 117 | stream.write(b"\x00\x00\x00") # if_tsresol (+padding) 118 | stream.write(b"\x00\x00\x00\x00") # end of options 119 | stream.write(b"\x00\x00\x00\x20") # block syze 120 | 121 | # ---------- Enhanced packet 122 | stream.write(b"\x00\x00\x00\x06") # block magic 123 | stream.write(b"\x00\x00\x00\x24") # block syze 124 | stream.write(b"\x00\x00\x00\x00") # interface id (first one, eth0) 125 | stream.write(struct.pack(">Q", int(timestamp))) # timestamp 126 | stream.write(b"\x00\x00\x00\x00") # Captured length 127 | stream.write(b"\x00\x00\x00\x00") # Original length 128 | # no packet data 129 | stream.write(b"\x00\x00\x00\x00") # Empty options 130 | stream.write(b"\x00\x00\x00\x24") # block syze 131 | 132 | return stream.getvalue() 133 | 134 | 135 | @pytest.mark.parametrize( 136 | "tsr_base,tsr_exp", 137 | [ 138 | (10, -6), 139 | (10, 0), 140 | (10, -3), 141 | (10, -9), # Bigger than this won't even fit.. 142 | (2, 0), 143 | (2, -5), 144 | (2, -10), 145 | (2, -20), 146 | ], 147 | ) 148 | def test_read_block_enhanced_packet_tsresol_bigendian(tsr_base, tsr_exp): 149 | data = _generate_file_with_tsresol(tsr_base, tsr_exp) 150 | scanner = FileScanner(io.BytesIO(data)) 151 | 152 | blocks = list(scanner) 153 | assert len(blocks) == 3 154 | 155 | assert isinstance(blocks[0], SectionHeader) 156 | assert blocks[0].endianness == ">" 157 | assert blocks[0].interfaces == {0: blocks[1]} 158 | 159 | assert isinstance(blocks[1], InterfaceDescription) 160 | assert len(blocks[1].options) == 1 # Just if_tsresol 161 | assert blocks[1].options["if_tsresol"] == pack_timestamp_resolution( 162 | tsr_base, tsr_exp 163 | ) 164 | 165 | assert isinstance(blocks[2], EnhancedPacket) 166 | assert blocks[2].section == blocks[0] 167 | assert blocks[2].interface_id == 0 168 | assert blocks[2].interface == blocks[1] 169 | 170 | resol = tsr_base**tsr_exp 171 | assert blocks[2].timestamp_resolution == resol 172 | assert blocks[2].timestamp == 1420070400.0 173 | -------------------------------------------------------------------------------- /tests/test_parse_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for errors during parsing 3 | """ 4 | 5 | import pytest 6 | 7 | from pcapng.blocks import SectionHeader 8 | 9 | 10 | def test_get_nonexistent_block_attribute(): 11 | shb = SectionHeader( 12 | raw=b"\x00\x01\x00\x00" b"\xff\xff\xff\xff\xff\xff\xff\xff" b"\x00\x00\x00\x00", 13 | endianness=">", 14 | ) 15 | 16 | assert shb.version == (1, 0) # check that parsing was successful 17 | 18 | with pytest.raises(AttributeError): 19 | shb.does_not_exist 20 | -------------------------------------------------------------------------------- /tests/test_parse_interface_block.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from pcapng.blocks import InterfaceDescription, SectionHeader 4 | from pcapng.scanner import FileScanner 5 | 6 | 7 | def test_read_block_interface_bigendian(): 8 | scanner = FileScanner( 9 | io.BytesIO( 10 | # ---------- Section header 11 | b"\x0a\x0d\x0d\x0a" # Magic number 12 | b"\x00\x00\x00\x20" # Block size (32 bytes) 13 | b"\x1a\x2b\x3c\x4d" # Magic number 14 | b"\x00\x01\x00\x00" # Version 15 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 16 | b"\x00\x00\x00\x00" # Empty options 17 | b"\x00\x00\x00\x20" # Block size (32 bytes) 18 | # ---------- Interface description 19 | b"\x00\x00\x00\x01" # block magic 20 | b"\x00\x00\x00\x40" # block syze (64 bytes) 21 | b"\x00\x01" # link type 22 | b"\x00\x00" # reserved block 23 | b"\x00\x00\xff\xff" # size limit 24 | b"\x00\x02\x00\x04" 25 | b"eth0" # if_name 26 | b"\x00\x09\x00\x01" 27 | b"\x06\x00\x00\x00" # if_tsresol (+padding) 28 | b"\x00\x0c\x00\x13" 29 | b"Linux 3.2.0-4-amd64\x00" # if_os 30 | b"\x00\x00\x00\x00" # end of options 31 | b"\x00\x00\x00\x40" # block syze (64 bytes) 32 | ) 33 | ) 34 | 35 | blocks = list(scanner) 36 | assert len(blocks) == 2 37 | 38 | assert isinstance(blocks[0], SectionHeader) 39 | assert blocks[0].endianness == ">" 40 | assert blocks[0].interfaces == {0: blocks[1]} 41 | 42 | assert isinstance(blocks[1], InterfaceDescription) 43 | assert blocks[1].link_type == 0x01 44 | assert blocks[1].link_type_description == "D/I/X and 802.3 Ethernet" 45 | assert blocks[1].snaplen == 0xFFFF 46 | assert blocks[1].options["if_name"] == "eth0" 47 | assert blocks[1].options["if_tsresol"] == b"\x06" 48 | assert blocks[1].timestamp_resolution == 1e-6 49 | assert blocks[1].options["if_os"] == "Linux 3.2.0-4-amd64" 50 | assert blocks[1].reserved == 0 51 | 52 | assert repr(blocks[1]) == ( 53 | "".format( 55 | options=repr(blocks[1].options), reserved=repr(blocks[1].reserved) 56 | ) 57 | ) 58 | 59 | 60 | def test_read_block_interface_nondefault_tsresol(): 61 | scanner = FileScanner( 62 | io.BytesIO( 63 | # ---------- Section header 64 | b"\x0a\x0d\x0d\x0a" # Magic number 65 | b"\x00\x00\x00\x20" # Block size (32 bytes) 66 | b"\x1a\x2b\x3c\x4d" # Magic number 67 | b"\x00\x01\x00\x00" # Version 68 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 69 | b"\x00\x00\x00\x00" # Empty options 70 | b"\x00\x00\x00\x20" # Block size (32 bytes) 71 | # ---------- Interface description 72 | b"\x00\x00\x00\x01" # block magic 73 | b"\x00\x00\x00\x20" # block syze (64 bytes) 74 | b"\x00\x01" # link type 75 | b"\x00\x00" # reserved block 76 | b"\x00\x00\xff\xff" # size limit 77 | b"\x00\x09\x00\x01" 78 | b"\x0c\x00\x00\x00" # if_tsresol (+padding) 79 | b"\x00\x00\x00\x00" # end of options 80 | b"\x00\x00\x00\x20" # block syze (64 bytes) 81 | ) 82 | ) 83 | 84 | blocks = list(scanner) 85 | assert len(blocks) == 2 86 | 87 | assert isinstance(blocks[1], InterfaceDescription) 88 | assert blocks[1].options["if_tsresol"] == b"\x0c" 89 | assert "if_tsresol" in blocks[1].options 90 | assert blocks[1].timestamp_resolution == 1e-12 91 | 92 | 93 | def test_read_block_interface_unknown_link_type(): 94 | scanner = FileScanner( 95 | io.BytesIO( 96 | # ---------- Section header 97 | b"\x0a\x0d\x0d\x0a" # Magic number 98 | b"\x00\x00\x00\x20" # Block size (32 bytes) 99 | b"\x1a\x2b\x3c\x4d" # Magic number 100 | b"\x00\x01\x00\x00" # Version 101 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 102 | b"\x00\x00\x00\x00" # Empty options 103 | b"\x00\x00\x00\x20" # Block size (32 bytes) 104 | # ---------- Interface description 105 | b"\x00\x00\x00\x01" # block magic 106 | b"\x00\x00\x00\x18" # block syze 107 | b"\xff\x01" # link type (unknown) 108 | b"\x00\x00" # reserved block 109 | b"\x00\x00\xff\xff" # size limit 110 | b"\x00\x00\x00\x00" # end of options 111 | b"\x00\x00\x00\x18" # block syze (64 bytes) 112 | ) 113 | ) 114 | 115 | blocks = list(scanner) 116 | assert len(blocks) == 2 117 | 118 | assert isinstance(blocks[1], InterfaceDescription) 119 | assert blocks[1].link_type == 0xFF01 120 | assert blocks[1].link_type_description == "Unknown link type: 0xff01" 121 | -------------------------------------------------------------------------------- /tests/test_parse_interface_stats.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from pcapng.blocks import InterfaceDescription, InterfaceStatistics, SectionHeader 4 | from pcapng.scanner import FileScanner 5 | 6 | 7 | def test_read_block_interface_stats_bigendian(): 8 | scanner = FileScanner( 9 | io.BytesIO( 10 | # ---------- Section header 11 | b"\x0a\x0d\x0d\x0a" # Magic number 12 | b"\x00\x00\x00\x20" # Block size (32 bytes) 13 | b"\x1a\x2b\x3c\x4d" # Magic number 14 | b"\x00\x01\x00\x00" # Version 15 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 16 | b"\x00\x00\x00\x00" # Empty options 17 | b"\x00\x00\x00\x20" # Block size (32 bytes) 18 | # ---------- Interface description 19 | b"\x00\x00\x00\x01" # block magic 20 | b"\x00\x00\x00\x40" # block syze (64 bytes) 21 | b"\x00\x01" # link type 22 | b"\x00\x00" # reserved block 23 | b"\x00\x00\xff\xff" # size limit 24 | b"\x00\x02\x00\x04" 25 | b"eth0" # if_name 26 | b"\x00\x09\x00\x01" 27 | b"\x06\x00\x00\x00" # if_tsresol (+padding) 28 | b"\x00\x0c\x00\x13" 29 | b"Linux 3.2.0-4-amd64\x00" # if_os 30 | b"\x00\x00\x00\x00" # End of options 31 | b"\x00\x00\x00\x40" # block syze (64 bytes) 32 | # ---------- Interface statistics 33 | b"\x00\x00\x00\x05" # Magic number 34 | b"\x00\x00\x00\x80" # block size (128 bytes) 35 | b"\x00\x00\x00\x00" # interface id 36 | b"\x00\x05\x0b\x5f\x61\xf8\x14\x40" # Timestamp 37 | b"\x00\x01\x00\x0a" 38 | b"A comment\x00\x00\x00" 39 | b"\x00\x02\x00\x08" 40 | b"\x00\x05\x0b\x5f\x64\xa6\xb9\x80" # isb_starttime # noqa 41 | b"\x00\x03\x00\x08" 42 | b"\x00\x05\x0b\x5f\x6b\x44\x73\x40" # isb_endtime 43 | b"\x00\x04\x00\x08" 44 | b"\x00\x00\x00\x00\x00\x01\x23\x45" # isb_ifrecv 45 | b"\x00\x05\x00\x08" 46 | b"\x00\x00\x00\x00\x00\x00\x00\x20" # isb_drop 47 | b"\x00\x06\x00\x08" 48 | b"\x00\x00\x00\x00\x00\x00\x0a\xbc" # isb_filteraccept # noqa 49 | b"\x00\x07\x00\x08" 50 | b"\x00\x00\x00\x00\x00\x00\x00\x33" # isb_osdrop 51 | b"\x00\x08\x00\x08" 52 | b"\x00\x00\x00\x00\x00\x0a\xbc\xde" # isb_usrdeliv 53 | b"\x00\x00\x00\x00" # End of options 54 | b"\x00\x00\x00\x80" # block size (16 bytes) 55 | ) 56 | ) 57 | 58 | blocks = list(scanner) 59 | assert len(blocks) == 3 60 | 61 | assert isinstance(blocks[0], SectionHeader) 62 | assert blocks[0].endianness == ">" 63 | assert blocks[0].interfaces == {0: blocks[1]} 64 | 65 | assert isinstance(blocks[1], InterfaceDescription) 66 | assert blocks[1].statistics is blocks[2] 67 | 68 | assert isinstance(blocks[2], InterfaceStatistics) 69 | assert blocks[2].timestamp == 0x050B5F61F81440 / 1e6 70 | assert blocks[2].options["isb_starttime"] == 0x050B5F64A6B980 # no resol! 71 | assert blocks[2].options["isb_endtime"] == 0x050B5F6B447340 # no resol! 72 | assert blocks[2].options["isb_ifrecv"] == 0x12345 73 | assert blocks[2].options["isb_ifdrop"] == 0x20 74 | assert blocks[2].options["isb_filteraccept"] == 0xABC 75 | assert blocks[2].options["isb_osdrop"] == 0x33 76 | assert blocks[2].options["isb_usrdeliv"] == 0xABCDE 77 | -------------------------------------------------------------------------------- /tests/test_parse_section_header.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from pcapng.blocks import SectionHeader 4 | from pcapng.scanner import FileScanner 5 | from pcapng.structs import Options 6 | 7 | 8 | def test_read_block_sectionheader_bigendian_empty_options(): 9 | scanner = FileScanner( 10 | io.BytesIO( 11 | b"\x0a\x0d\x0d\x0a" # Magic number 12 | b"\x00\x00\x00\x20" # Block size (32 bytes) 13 | b"\x1a\x2b\x3c\x4d" # Magic number 14 | b"\x00\x01\x00\x00" # Version 15 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 16 | b"\x00\x00\x00\x00" # Empty options 17 | b"\x00\x00\x00\x20" # Block size (32 bytes) 18 | ) 19 | ) 20 | 21 | blocks = list(scanner) 22 | assert len(blocks) == 1 23 | block = blocks[0] 24 | 25 | assert isinstance(block, SectionHeader) 26 | assert block.endianness == ">" 27 | assert block.version == (1, 0) 28 | assert block.length == -1 29 | assert isinstance(block.options, Options) 30 | assert len(block.options) == 0 31 | assert block.interfaces == {} 32 | 33 | 34 | def test_read_block_sectionheader_littleendian_empty_options(): 35 | scanner = FileScanner( 36 | io.BytesIO( 37 | b"\x0a\x0d\x0d\x0a" # Magic number 38 | b"\x20\x00\x00\x00" # Block size (32 bytes) 39 | b"\x4d\x3c\x2b\x1a" # Magic number 40 | b"\x01\x00\x00\x00" # Version 41 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 42 | b"\x00\x00\x00\x00" # Empty options 43 | b"\x20\x00\x00\x00" # Block size (32 bytes) 44 | ) 45 | ) 46 | 47 | blocks = list(scanner) 48 | assert len(blocks) == 1 49 | block = blocks[0] 50 | 51 | assert isinstance(block, SectionHeader) 52 | assert block.endianness == "<" 53 | assert block.version == (1, 0) 54 | assert block.length == -1 55 | assert isinstance(block.options, Options) 56 | assert len(block.options) == 0 57 | assert block.interfaces == {} 58 | 59 | 60 | def test_read_block_sectionheader_bigendian_missing_options(): 61 | scanner = FileScanner( 62 | io.BytesIO( 63 | b"\x0a\x0d\x0d\x0a" # Magic number 64 | b"\x00\x00\x00\x1c" # Block size (32 bytes) 65 | b"\x1a\x2b\x3c\x4d" # Byte order 66 | b"\x00\x01\x00\x00" # Version 67 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 68 | b"" # Missing options 69 | b"\x00\x00\x00\x1c" # Block size (32 bytes) 70 | ) 71 | ) 72 | 73 | blocks = list(scanner) 74 | assert len(blocks) == 1 75 | block = blocks[0] 76 | 77 | assert isinstance(block, SectionHeader) 78 | assert block.endianness == ">" 79 | assert block.version == (1, 0) 80 | assert block.length == -1 81 | assert isinstance(block.options, Options) 82 | assert len(block.options) == 0 83 | assert block.interfaces == {} 84 | 85 | 86 | def test_read_block_sectionheader_littleendian_missing_options(): 87 | scanner = FileScanner( 88 | io.BytesIO( 89 | b"\x0a\x0d\x0d\x0a" # Magic number 90 | b"\x1c\x00\x00\x00" # Block size (32 bytes) 91 | b"\x4d\x3c\x2b\x1a" # Byte order 92 | b"\x01\x00\x00\x00" # Version 93 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 94 | b"" # Missing options 95 | b"\x1c\x00\x00\x00" # Block size (32 bytes) 96 | ) 97 | ) 98 | 99 | blocks = list(scanner) 100 | assert len(blocks) == 1 101 | block = blocks[0] 102 | 103 | assert isinstance(block, SectionHeader) 104 | assert block.endianness == "<" 105 | assert block.version == (1, 0) 106 | assert block.length == -1 107 | assert isinstance(block.options, Options) 108 | assert len(block.options) == 0 109 | assert block.interfaces == {} 110 | 111 | 112 | def test_read_block_sectionheader_bigendian_with_options(): 113 | scanner = FileScanner( 114 | io.BytesIO( 115 | b"\x0a\x0d\x0d\x0a" # Magic number 116 | b"\x00\x00\x00\x60" # Block size (96 bytes) 117 | b"\x1a\x2b\x3c\x4d" # Magic number 118 | b"\x00\x01\x00\x00" # Version 119 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 120 | # Options 121 | b"\x00\x01\x00\x0e" 122 | b"Just a comment\x00\x00" 123 | b"\x00\x02\x00\x0b" 124 | b"My Computer\x00" 125 | b"\x00\x03\x00\x05" 126 | b"My OS\x00\x00\x00" 127 | b"\x00\x04\x00\x0a" 128 | b"A fake app\x00\x00" 129 | b"\x00\x00\x00\x00" 130 | b"\x00\x00\x00\x60" # Block size (96 bytes) 131 | ) 132 | ) 133 | 134 | blocks = list(scanner) 135 | assert len(blocks) == 1 136 | block = blocks[0] 137 | 138 | assert isinstance(block, SectionHeader) 139 | assert block.endianness == ">" 140 | assert block.version == (1, 0) 141 | assert block.length == -1 142 | assert isinstance(block.options, Options) 143 | assert len(block.options) == 4 144 | assert block.options["opt_comment"] == "Just a comment" 145 | assert block.interfaces == {} 146 | 147 | assert repr(block) == ( 148 | "".format( 149 | repr(block.options) 150 | ) 151 | ) 152 | 153 | 154 | def test_read_block_sectionheader_littleendian_with_options(): 155 | scanner = FileScanner( 156 | io.BytesIO( 157 | b"\x0a\x0d\x0d\x0a" # Magic number 158 | b"\x60\x00\x00\x00" # Block size (96 bytes) 159 | b"\x4d\x3c\x2b\x1a" # Magic number 160 | b"\x01\x00\x00\x00" # Version 161 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # Undefined section length 162 | # Options 163 | b"\x01\x00\x0e\x00Just a comment\x00\x00" 164 | b"\x02\x00\x0b\x00My Computer\x00" 165 | b"\x03\x00\x05\x00My OS\x00\x00\x00" 166 | b"\x04\x00\x0a\x00A fake app\x00\x00" 167 | b"\x00\x00\x00\x00" 168 | b"\x60\x00\x00\x00" # Block size (96 bytes) 169 | ) 170 | ) 171 | 172 | blocks = list(scanner) 173 | assert len(blocks) == 1 174 | block = blocks[0] 175 | 176 | assert isinstance(block, SectionHeader) 177 | assert block.endianness == "<" 178 | assert block.version == (1, 0) 179 | assert block.length == -1 180 | assert isinstance(block.options, Options) 181 | assert len(block.options) == 4 182 | assert block.options["opt_comment"] == "Just a comment" 183 | assert block.interfaces == {} 184 | 185 | assert repr(block) == ( 186 | "".format( 187 | repr(block.options) 188 | ) 189 | ) 190 | -------------------------------------------------------------------------------- /tests/test_parse_wireshark_capture_files.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pcapng.blocks import InterfaceDescription, ObsoletePacket, SectionHeader 4 | from pcapng.scanner import FileScanner 5 | 6 | 7 | def test_sample_test001_ntar(): 8 | with open("test_data/test001.ntar", "rb") as fp: 9 | scanner = FileScanner(fp) 10 | blocks = list(scanner) 11 | 12 | # There is just a section header 13 | assert len(blocks) == 1 14 | 15 | assert blocks[0].endianness == "<" 16 | assert blocks[0].version == (1, 0) 17 | assert blocks[0].length == -1 18 | assert len(blocks[0].options) == 0 19 | assert len(blocks[0].interfaces) == 0 20 | 21 | 22 | def test_sample_test002_ntar(): 23 | with open("test_data/test002.ntar", "rb") as fp: 24 | scanner = FileScanner(fp) 25 | blocks = list(scanner) 26 | 27 | # Section header, interface description 28 | assert len(blocks) == 2 29 | 30 | assert isinstance(blocks[0], SectionHeader) 31 | assert blocks[0].endianness == "<" 32 | assert blocks[0].version == (1, 0) 33 | assert blocks[0].length == -1 34 | assert len(blocks[0].options) == 0 35 | assert len(blocks[0].interfaces) == 1 36 | 37 | assert isinstance(blocks[1], InterfaceDescription) 38 | assert blocks[1].link_type == 0 # Unknown link type 39 | assert blocks[1].snaplen == 0 40 | assert len(blocks[1].options) == 0 41 | 42 | 43 | def test_sample_test003_ntar(): 44 | with open("test_data/test003.ntar", "rb") as fp: 45 | scanner = FileScanner(fp) 46 | blocks = list(scanner) 47 | 48 | # Section header, interface description 49 | assert len(blocks) == 2 50 | 51 | assert isinstance(blocks[0], SectionHeader) 52 | assert blocks[0].endianness == "<" 53 | assert blocks[0].version == (1, 0) 54 | assert blocks[0].length == -1 55 | assert len(blocks[0].options) == 0 56 | assert len(blocks[0].interfaces) == 1 57 | 58 | assert isinstance(blocks[1], InterfaceDescription) 59 | assert blocks[1].link_type == 0x04D8 # ??? 60 | assert blocks[1].snaplen == 0x7C 61 | assert len(blocks[1].options) == 0 62 | 63 | 64 | def test_sample_test004_ntar(): 65 | with open("test_data/test004.ntar", "rb") as fp: 66 | scanner = FileScanner(fp) 67 | blocks = list(scanner) 68 | 69 | # Section header 70 | assert len(blocks) == 1 71 | 72 | assert isinstance(blocks[0], SectionHeader) 73 | assert blocks[0].endianness == "<" 74 | assert blocks[0].version == (1, 0) 75 | assert blocks[0].length == -1 76 | 77 | assert len(blocks[0].options) == 2 78 | assert blocks[0].options["shb_os"] == "Windows XP\x00" # (why NULL?) 79 | assert blocks[0].options["shb_userappl"] == "Test004.exe\x00" 80 | 81 | assert len(blocks[0].interfaces) == 0 82 | 83 | 84 | def test_sample_test005_ntar(): 85 | with open("test_data/test005.ntar", "rb") as fp: 86 | scanner = FileScanner(fp) 87 | blocks = list(scanner) 88 | 89 | # Section header, interface description 90 | assert len(blocks) == 2 91 | 92 | assert isinstance(blocks[0], SectionHeader) 93 | assert blocks[0].endianness == "<" 94 | assert blocks[0].version == (1, 0) 95 | assert blocks[0].length == -1 96 | assert len(blocks[0].options) == 0 97 | assert len(blocks[0].interfaces) == 1 98 | 99 | assert isinstance(blocks[1], InterfaceDescription) 100 | assert blocks[1].link_type == 0x04D8 # ??? 101 | assert blocks[1].snaplen == 0x7C 102 | assert len(blocks[1].options) == 2 103 | 104 | assert ( 105 | blocks[1].options.get_raw("if_speed") == b"\x00\xe4\x0b\x54\x02\x00\x00\x00" 106 | ) # noqa 107 | assert blocks[1].options["if_speed"] == 0x00000002540BE400 108 | assert blocks[1].options["if_speed"] == (10**10) # 10Gbit 109 | 110 | assert blocks[1].options["if_description"] == "Stupid ethernet interface\x00" 111 | 112 | 113 | @pytest.mark.parametrize( 114 | "filename", 115 | [ 116 | pytest.param("test_data/test006.ntar", marks=pytest.mark.xfail), 117 | "test_data/test006-fixed.ntar", 118 | ], 119 | ) 120 | def test_sample_test006_ntar(filename): 121 | # Note: See the comment below this function 122 | # test006.ntar is reporting an incorrect size, which causes the 123 | # test to fail. Is this the expected behavior? 124 | 125 | with open(filename, "rb") as fp: 126 | scanner = FileScanner(fp) 127 | 128 | blocks = list(scanner) 129 | 130 | # Section header, interface description, then what?? 131 | assert len(blocks) == 3 132 | 133 | assert isinstance(blocks[0], SectionHeader) 134 | assert blocks[0].endianness == "<" 135 | assert blocks[0].version == (1, 0) 136 | assert blocks[0].length == -1 137 | assert len(blocks[0].options) == 0 138 | assert len(blocks[0].interfaces) == 1 139 | 140 | assert isinstance(blocks[1], InterfaceDescription) 141 | assert blocks[1].link_type == 2 142 | assert blocks[1].snaplen == 96 143 | assert len(blocks[1].options) == 2 144 | 145 | assert blocks[1].options["if_speed"] == (10**8) # 100Mbit 146 | 147 | assert blocks[1].options["if_description"] == "Stupid ethernet interface\x00" 148 | 149 | assert isinstance(blocks[2], ObsoletePacket) 150 | assert blocks[2].interface_id == 0 151 | assert blocks[2].options["pack_flags"].inout == "NA" 152 | assert blocks[2].options["pack_flags"].casttype == "NA" 153 | assert blocks[2].options["pack_flags"].fcslen == 0 154 | assert blocks[2].options["pack_flags"].reserved == 0 155 | assert blocks[2].options["pack_flags"].err_16 is False 156 | assert blocks[2].options["pack_flags"].err_17 is False 157 | assert blocks[2].options["pack_flags"].err_18 is False 158 | assert blocks[2].options["pack_flags"].err_19 is False 159 | assert blocks[2].options["pack_flags"].err_20 is False 160 | assert blocks[2].options["pack_flags"].err_21 is False 161 | assert blocks[2].options["pack_flags"].err_22 is False 162 | assert blocks[2].options["pack_flags"].err_23 is False 163 | assert blocks[2].options["pack_flags"].err_crc is False 164 | assert blocks[2].options["pack_flags"].err_long is False 165 | assert blocks[2].options["pack_flags"].err_short is False 166 | assert blocks[2].options["pack_flags"].err_frame_gap is False 167 | assert blocks[2].options["pack_flags"].err_frame_align is False 168 | assert blocks[2].options["pack_flags"].err_frame_delim is False 169 | assert blocks[2].options["pack_flags"].err_preamble is False 170 | assert blocks[2].options["pack_flags"].err_symbol is False 171 | 172 | 173 | # ============================================================ 174 | # Dissection of test006.ntar 175 | # 176 | # PROBLEM: Total size of packet block is incorrectly reported 177 | # to be one byte shorter than it actually is! 178 | # ============================================================ 179 | 180 | # -------------------- Section header -------------------- 181 | 182 | # 00000000: 0a0d 0d0a Magic number 183 | # 00000000: 1c00 0000 Block size (28) 184 | # 00000000: 4d3c 2b1a Byte order (LE) 185 | # 00000000: 0100 0000 Version (1, 0) 186 | # 00000010: ffff ffff ffff ffff Section size (-1) 187 | # (No options) 188 | # 00000010: 1c00 0000 Block size (28) 189 | 190 | # -------------------- Interface description -------------------- 191 | 192 | # 00000010: 0100 0000 Block Magic 193 | 194 | # 00000020: 4400 0000 Block total length (68) 195 | # 00000020: 0200 Link type (2) 196 | # 00000020: 0000 Reserved (0) 197 | # 00000020: 6000 0000 Snapshot length 198 | 199 | # 00000020: 0300 1a00 Option 3 - 26 bytes 200 | # 00000030: 5374 7570 6964 2065 7468 6572 6e65 7420 Stupid ethernet 201 | # 00000040: 696e 7465 7266 6163 6500 0000 interface 202 | 203 | # 00000040: 0800 0800 Option 8 - 8 bytes 204 | # 00000050: 00e1 f505 0000 0000 (speed = 100Mbps) 205 | 206 | # 00000050: 0000 0000 End of options block 207 | # 00000050: 4400 0000 Block total length (68) 208 | 209 | # -------------------- Packet (Obsolete) -------------------- 210 | 211 | # 00000060: 0200 0000 Block Magic 212 | # 00000060: a700 0000 Block size (167(!??)) 213 | # 00000060: 0000 Interface id (0) 214 | # 00000060: 0000 Drops count 215 | # 00000060: 0000 0000 Timestamp (high) 216 | # 00000070: 0000 0000 Timestamp (low) 217 | # 00000070: 7b00 0000 Captured len (123) [pad 1] 218 | # 00000070: e803 0000 Packet len (1000) 219 | 220 | # 00000070: 6853 11f3 ....{.......hS.. [4] 221 | # 00000080: 3b00 0000 978f 00f3 3b00 0000 0000 0000 ;.......;....... [20] 222 | # 00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................ [36] 223 | # 000000a0: 0000 0000 0100 0000 0000 0000 d0f1 ffbf ................ [52] 224 | # 000000b0: 7f00 0000 d04f 11f3 3b00 0000 6005 00f3 .....O..;...`... [68] 225 | # 000000c0: 3b00 0000 fc06 00f3 3b00 0000 6002 00f3 ;.......;...`... [84] 226 | # 000000d0: 3b00 0000 5806 4000 0000 0000 6853 11f3 ;...X.@.....hS.. [100] 227 | # 000000e0: 3b00 0000 6853 11f3 0200 0000 0000 0000 ;...hS.......... [116] 228 | # 000000f0: 0000 0000 0000 0000 ................ [124] 229 | 230 | # 000000f0: 0200 0400 Option 2 - 4 bytes 231 | # 000000f0: 0000 0000 0x00000000 232 | # 00000100: 0000 0000 Options end marker 233 | 234 | # 00000100: a700 0000 Block size (167) 235 | 236 | 237 | def test_sample_test007_ntar(): 238 | with open("test_data/test007.ntar", "rb") as fp: 239 | scanner = FileScanner(fp) 240 | for entry in scanner: 241 | pass 242 | 243 | 244 | def test_sample_test008_ntar(): 245 | with open("test_data/test008.ntar", "rb") as fp: 246 | scanner = FileScanner(fp) 247 | for entry in scanner: 248 | pass 249 | 250 | 251 | def test_sample_test009_ntar(): 252 | with open("test_data/test009.ntar", "rb") as fp: 253 | scanner = FileScanner(fp) 254 | for entry in scanner: 255 | pass 256 | 257 | 258 | def test_sample_test010_ntar(): 259 | with open("test_data/test010.ntar", "rb") as fp: 260 | scanner = FileScanner(fp) 261 | for entry in scanner: 262 | pass 263 | -------------------------------------------------------------------------------- /tests/test_structs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test unpacking structs 3 | """ 4 | 5 | import io 6 | import struct 7 | 8 | import pytest 9 | 10 | from pcapng.exceptions import BadMagic, CorruptedFile, StreamEmpty, TruncatedFile 11 | from pcapng.structs import ( 12 | IntField, 13 | ListField, 14 | NameResolutionRecordField, 15 | Option, 16 | Options, 17 | OptionsField, 18 | PacketBytes, 19 | RawBytes, 20 | read_block_data, 21 | read_bytes, 22 | read_bytes_padded, 23 | read_int, 24 | read_options, 25 | read_section_header, 26 | struct_decode, 27 | ) 28 | 29 | 30 | def test_read_int(): 31 | # 16bit, signed, positive 32 | assert read_int(io.BytesIO(b"\x12\x34"), 16, True, ">") == 0x1234 33 | assert read_int(io.BytesIO(b"\x12\x34"), 16, True, "<") == 0x3412 34 | 35 | assert read_int(io.BytesIO(b"\x12\x34extra"), 16, True, ">") == 0x1234 36 | assert read_int(io.BytesIO(b"\x12\x34extra"), 16, True, "<") == 0x3412 37 | 38 | # 16bit, signed, negative 39 | assert read_int(io.BytesIO(b"\xed\xcc"), 16, True, ">") == -0x1234 40 | assert read_int(io.BytesIO(b"\xcc\xed"), 16, True, "<") == -0x1234 41 | 42 | assert read_int(io.BytesIO(b"\xed\xccextra"), 16, True, ">") == -0x1234 43 | assert read_int(io.BytesIO(b"\xcc\xedextra"), 16, True, "<") == -0x1234 44 | 45 | # 16bit, unsigned 46 | assert read_int(io.BytesIO(b"\x12\x34"), 16, False, ">") == 0x1234 47 | assert read_int(io.BytesIO(b"\x12\x34"), 16, False, "<") == 0x3412 48 | 49 | assert read_int(io.BytesIO(b"\x12\x34extra"), 16, False, ">") == 0x1234 50 | assert read_int(io.BytesIO(b"\x12\x34extra"), 16, False, "<") == 0x3412 51 | 52 | # ..do we really need to test other sizes? 53 | assert ( 54 | read_int(io.BytesIO(b"\x12\x34\x56\x78"), 32, False, ">") == 0x12345678 55 | ) # noqa 56 | assert ( 57 | read_int(io.BytesIO(b"\x12\x34\x56\x78"), 32, False, "<") == 0x78563412 58 | ) # noqa 59 | assert ( 60 | read_int(io.BytesIO(b"\x12\x34\x56\x78"), 32, True, ">") == 0x12345678 61 | ) # noqa 62 | assert ( 63 | read_int(io.BytesIO(b"\x12\x34\x56\x78"), 32, True, "<") == 0x78563412 64 | ) # noqa 65 | 66 | 67 | def test_read_int_empty_stream(): 68 | with pytest.raises(StreamEmpty): 69 | read_int(io.BytesIO(b""), 32) 70 | 71 | 72 | def test_read_int_truncated_stream(): 73 | with pytest.raises(TruncatedFile): 74 | read_int(io.BytesIO(b"AB"), 32) 75 | 76 | 77 | def test_read_section_header_big_endian(): 78 | data = io.BytesIO( 79 | # '\x0a\x0d\x0d\x0a' # magic number has already been read.. 80 | b"\x00\x00\x00\x1c" # block length (28 bytes) 81 | b"\x1a\x2b\x3c\x4d" # byte order magic [it's big endian!] 82 | b"\x00\x01\x00\x00" # version 1.0 83 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # section length unknown 84 | b"" # no options here! 85 | b"\x00\x00\x00\x1c" 86 | ) # block length, again 87 | 88 | block = read_section_header(data) 89 | assert block["endianness"] == ">" 90 | assert block["data"] == b"\x00\x01\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff" 91 | 92 | 93 | def test_read_section_header_little_endian(): 94 | data = io.BytesIO( 95 | # '\x0a\x0d\x0d\x0a' # magic number 96 | b"\x1c\x00\x00\x00" # block length (28 bytes) 97 | b"\x4d\x3c\x2b\x1a" # byte order magic [it's big endian!] 98 | b"\x01\x00\x00\x00" # version 1.0 99 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # section length unknown 100 | b"" # no options here! 101 | b"\x1c\x00\x00\x00" 102 | ) # block length, again 103 | 104 | block = read_section_header(data) 105 | assert block["endianness"] == "<" 106 | assert block["data"] == b"\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff" 107 | 108 | 109 | def test_read_section_header_bad_order_magic(): 110 | data = io.BytesIO( 111 | # '\x0a\x0d\x0d\x0a' # magic number 112 | b"\x1c\x00\x00\x00" # block length (28 bytes) 113 | b"\x0B\xAD\xBE\xEF" # byte order magic [it's big endian!] 114 | b"\x01\x00\x00\x00" # version 1.0 115 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # section length unknown 116 | b"" # no options here! 117 | b"\x1c\x00\x00\x00" 118 | ) # block length, again 119 | 120 | with pytest.raises(BadMagic) as ctx: 121 | read_section_header(data) 122 | 123 | assert str(ctx.value) == ( 124 | "Wrong byte order magic: got 0x0BADBEEF, " "expected 0x1A2B3C4D or 0x4D3C2B1A" 125 | ) 126 | 127 | 128 | def test_read_section_header_mismatching_lengths(): 129 | data = io.BytesIO( 130 | # '\x0a\x0d\x0d\x0a' # magic number 131 | b"\x00\x00\x00\x1c" # block length (28 bytes) 132 | b"\x1a\x2b\x3c\x4d" # byte order magic [it's big endian!] 133 | b"\x00\x01\x00\x00" # version 1.0 134 | b"\xff\xff\xff\xff\xff\xff\xff\xff" # section length unknown 135 | b"" # no options here! 136 | b"\x00\x00\x00\x00" 137 | ) # block length, again but WRONG! 138 | 139 | with pytest.raises(CorruptedFile) as ctx: 140 | read_section_header(data) 141 | 142 | assert str(ctx.value) == "Mismatching block lengths: 28 and 0" 143 | 144 | 145 | def test_read_block_data_big_endian(): 146 | # No need for padding; size = 4 bytes (size 0x10) 147 | data = io.BytesIO(b"\x00\x00\x00\x10" b"1234" b"\x00\x00\x00\x10") 148 | assert read_block_data(data, ">") == b"1234" 149 | 150 | # Base size: 0x0c (12); payload size: 0x05; total: 0x11 (17) 151 | data = io.BytesIO(b"\x00\x00\x00\x11" b"12345XXX" b"\x00\x00\x00\x11") 152 | assert read_block_data(data, ">") == b"12345" 153 | 154 | 155 | def test_read_block_data_little_endian(): 156 | # No need for padding; size = 4 bytes (size 0x10) 157 | data = io.BytesIO(b"\x10\x00\x00\x00" b"1234" b"\x10\x00\x00\x00\x10") 158 | assert read_block_data(data, "<") == b"1234" 159 | 160 | # Base size: 0x0c (12); payload size: 0x05; total: 0x11 (17) 161 | data = io.BytesIO(b"\x11\x00\x00\x00" b"12345XXX" b"\x11\x00\x00\x00") 162 | assert read_block_data(data, "<") == b"12345" 163 | 164 | 165 | def test_read_block_data_mismatching_lengths(): 166 | data = io.BytesIO(b"\x00\x00\x00\x11" b"12345XXX" b"\xff\x00\x00\x11") 167 | with pytest.raises(CorruptedFile) as ctx: 168 | read_block_data(data, ">") 169 | 170 | assert str(ctx.value) == "Mismatching block lengths: 17 and 4278190097" 171 | 172 | 173 | def test_read_bytes(): 174 | data = io.BytesIO(b"foobar") 175 | assert read_bytes(data, 3) == b"foo" 176 | assert read_bytes(data, 3) == b"bar" 177 | 178 | data = io.BytesIO(b"foo") 179 | with pytest.raises(TruncatedFile): 180 | read_bytes(data, 4) 181 | 182 | data = io.BytesIO(b"") 183 | with pytest.raises(StreamEmpty): 184 | read_bytes(data, 4) 185 | 186 | data = io.BytesIO(b"") 187 | assert read_bytes(data, 0) == b"" 188 | 189 | 190 | def test_read_bytes_padded(): 191 | data = io.BytesIO(b"spam") 192 | assert read_bytes_padded(data, 4) == b"spam" 193 | 194 | data = io.BytesIO(b"spameggsbaconXXX") 195 | assert read_bytes_padded(data, 4) == b"spam" 196 | assert read_bytes_padded(data, 4) == b"eggs" 197 | assert read_bytes_padded(data, 5) == b"bacon" 198 | 199 | data = io.BytesIO(b"fooXbarX") 200 | assert data.tell() == 0 201 | assert read_bytes_padded(data, 3) == b"foo" 202 | assert data.tell() == 4 203 | assert read_bytes_padded(data, 3) == b"bar" 204 | 205 | data = io.BytesIO(b"foobar") 206 | data.read(1) 207 | assert data.tell() == 1 208 | with pytest.raises(RuntimeError): 209 | read_bytes_padded(data, 3) 210 | 211 | 212 | def test_decode_simple_struct(): 213 | schema = [ 214 | ("rawbytes", RawBytes(12), b""), 215 | ("int32s", IntField(32, True), 0), 216 | ("int32u", IntField(32, False), 0), 217 | ("int16s", IntField(16, True), 0), 218 | ("int16u", IntField(16, False), 0), 219 | ] 220 | 221 | stream = io.BytesIO() 222 | stream.write(b"Hello world!") 223 | stream.write(struct.pack(">i", -1234)) 224 | stream.write(struct.pack(">I", 1234)) 225 | stream.write(struct.pack(">h", -789)) 226 | stream.write(struct.pack(">H", 789)) 227 | 228 | stream.seek(0) 229 | decoded = struct_decode(schema, stream, ">") 230 | 231 | assert decoded["rawbytes"] == b"Hello world!" 232 | assert decoded["int32s"] == -1234 233 | assert decoded["int32u"] == 1234 234 | assert decoded["int16s"] == -789 235 | assert decoded["int16u"] == 789 236 | 237 | 238 | def test_read_options(): 239 | data = io.BytesIO( 240 | b"\x00\x01\x00\x0cHello world!" 241 | b"\x00\x01\x00\x0fSpam eggs bacon\x00" 242 | b"\x00\x02\x00\x0fSome other text\x00" 243 | b"\x00\x00\x00\x00" 244 | ) 245 | 246 | options = read_options(data, ">") 247 | assert options == [ 248 | (1, b"Hello world!"), 249 | (1, b"Spam eggs bacon"), 250 | (2, b"Some other text"), 251 | ] 252 | 253 | 254 | def test_read_options_2(): 255 | data = io.BytesIO( 256 | b"\x00\x01\x00\x0e" 257 | b"Just a comment\x00\x00" 258 | b"\x00\x02\x00\x0b" 259 | b"My Computer\x00" 260 | b"\x00\x03\x00\x05" 261 | b"My OS\x00\x00\x00" 262 | b"\x00\x04\x00\x0a" 263 | b"A fake app\x00\x00" 264 | b"\x00\x00\x00\x00" 265 | ) 266 | 267 | options = read_options(data, ">") 268 | assert options == [ 269 | (1, b"Just a comment"), 270 | (2, b"My Computer"), 271 | (3, b"My OS"), 272 | (4, b"A fake app"), 273 | ] 274 | 275 | 276 | def test_options_object(): 277 | schema = [ 278 | Option(2, "spam", "bytes"), 279 | Option(3, "eggs", "u32"), 280 | Option(4, "bacon", "string"), 281 | Option(5, "missing"), 282 | ] 283 | 284 | raw_options = [ 285 | (1, b"Comment #1"), 286 | (1, b"Comment #2"), 287 | (2, b"I love spam spam spam!"), 288 | (3, b"\x00\x00\x01\x00"), 289 | (4, b"Bacon is delicious!"), 290 | (20, b"Something different"), 291 | (2988, b"\x00\x00\x00\x01Some Custom Data"), 292 | (2989, b"\x00\x00\x00\x01\x01\x02"), 293 | ] 294 | 295 | options = Options(schema=schema, data=raw_options, endianness=">") 296 | 297 | assert options["opt_comment"] == "Comment #1" 298 | assert options[1] == "Comment #1" 299 | assert options.get_all("opt_comment") == ["Comment #1", "Comment #2"] 300 | assert isinstance(options["opt_comment"], str) 301 | 302 | assert options["spam"] == b"I love spam spam spam!" 303 | assert isinstance(options["spam"], bytes) 304 | 305 | assert options["eggs"] == 0x100 306 | assert isinstance(options["eggs"], int) 307 | 308 | assert options["bacon"] == "Bacon is delicious!" 309 | assert isinstance(options["bacon"], str) 310 | 311 | assert options["custom_str_safe"] == (1, "Some Custom Data") 312 | assert isinstance(options["custom_str_safe"][0], int) 313 | assert isinstance(options["custom_str_safe"][1], str) 314 | 315 | assert options["custom_bytes_safe"] == (1, b"\x01\x02") 316 | assert isinstance(options["custom_bytes_safe"][0], int) 317 | assert isinstance(options["custom_bytes_safe"][1], bytes) 318 | 319 | with pytest.raises(KeyError): 320 | options["missing"] 321 | 322 | with pytest.raises(KeyError): 323 | options[8] 324 | 325 | with pytest.raises(KeyError): 326 | options["Something completely missing"] 327 | 328 | with pytest.raises(KeyError): 329 | options[12345] 330 | 331 | assert options[20] == b"Something different" 332 | 333 | # Check length / keys 334 | assert len(options) == 7 335 | assert set(options.keys()) == set( 336 | [ 337 | "opt_comment", 338 | "spam", 339 | "eggs", 340 | "bacon", 341 | "custom_str_safe", 342 | "custom_bytes_safe", 343 | 20, 344 | ] 345 | ) 346 | 347 | # Check "in" and "not in" 348 | assert "opt_comment" in options 349 | assert "spam" in options 350 | assert "eggs" in options 351 | assert "bacon" in options 352 | assert "custom_str_safe" in options 353 | assert "custom_bytes_safe" in options 354 | assert 20 in options 355 | assert "missing" not in options 356 | assert "something different" not in options 357 | 358 | assert 1 in options 359 | assert 2 in options 360 | assert 3 in options 361 | assert 4 in options 362 | assert 2988 in options 363 | assert 2989 in options 364 | assert 5 not in options 365 | assert 12345 not in options 366 | 367 | 368 | def test_unpack_dummy_packet(): 369 | schema = [ 370 | ("a_string", RawBytes(8), ""), 371 | ("a_number", IntField(32, False), 0), 372 | ("options", OptionsField([]), None), 373 | ("pb_captured_len", IntField(32, False), 0), 374 | ("pb_orig_len", IntField(32, False), 0), 375 | ("packet_data", PacketBytes("pb_captured_len"), b""), 376 | ("spb_orig_len", IntField(32, False), 0), 377 | ("simple_packet_data", PacketBytes("spb_orig_len"), b""), 378 | ("name_res", ListField(NameResolutionRecordField()), []), 379 | ("another_number", IntField(32, False), 0), 380 | ] 381 | 382 | # Note: NULLs are for padding! 383 | data = io.BytesIO( 384 | b"\x01\x23\x45\x67\x89\xab\xcd\xef" 385 | b"\x00\x00\x01\x00" 386 | # Options 387 | b"\x00\x01\x00\x0cHello world!" 388 | b"\x00\x01\x00\x0fSpam eggs bacon\x00" 389 | b"\x00\x02\x00\x0fSome other text\x00" 390 | b"\x00\x00\x00\x00" 391 | # Enhanced Packet data 392 | b"\x00\x00\x00\x12" 393 | b"\x00\x01\x00\x00" 394 | b"These are 18 bytes\x00\x00" 395 | # Simple packet data 396 | b"\x00\x00\x00\x0d" 397 | b"Simple packet\x00\x00\x00" 398 | # List of name resolution items 399 | b"\x00\x01" # IPv4 400 | b"\x00\x13" # Length: 19bytes 401 | b"\x0a\x22\x33\x44www.example.com\x00" # 19 bytes (10.34.51.68) 402 | b"\x00\x01" # IPv4 403 | b"\x00\x13" # Length: 19bytes 404 | b"\xc0\xa8\x14\x01www.example.org\x00" # 19 bytes (192.168.20.1) 405 | b"\x00\x02" # IPv6 406 | b"\x00\x1e" # 30 bytes 407 | b"\x00\x11\x22\x33\x44\x55\x66\x77" 408 | b"\x88\x99\xaa\xbb\xcc\xdd\xee\xff" 409 | b"v6.example.net\x00\x00" 410 | b"\x00\x00\x00\x00" # End marker 411 | # Another number, to check end 412 | b"\xaa\xbb\xcc\xdd" 413 | ) 414 | 415 | unpacked = struct_decode(schema, data, endianness=">") 416 | assert unpacked["a_string"] == b"\x01\x23\x45\x67\x89\xab\xcd\xef" 417 | assert unpacked["a_number"] == 0x100 418 | 419 | assert isinstance(unpacked["options"], Options) 420 | assert len(unpacked["options"]) == 2 421 | assert unpacked["options"]["opt_comment"] == "Hello world!" 422 | assert unpacked["options"][2] == b"Some other text" 423 | 424 | assert unpacked["pb_captured_len"] == 0x12 425 | assert unpacked["pb_orig_len"] == 0x10000 426 | assert unpacked["packet_data"] == b"These are 18 bytes" 427 | 428 | assert unpacked["spb_orig_len"] == 13 429 | assert unpacked["simple_packet_data"] == b"Simple packet" 430 | 431 | assert unpacked["name_res"] == [ 432 | {"address": "10.34.51.68", "names": ["www.example.com"], "type": 1}, 433 | {"address": "192.168.20.1", "names": ["www.example.org"], "type": 1}, 434 | { 435 | "type": 2, 436 | "address": "11:2233:4455:6677:8899:aabb:ccdd:eeff", 437 | "names": ["v6.example.net"], 438 | }, 439 | ] 440 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pcapng.utils import ( 2 | pack_timestamp_resolution, 3 | unpack_euiaddr, 4 | unpack_ipv4, 5 | unpack_ipv6, 6 | unpack_macaddr, 7 | unpack_timestamp_resolution, 8 | ) 9 | 10 | 11 | def test_unpack_ipv4(): 12 | assert unpack_ipv4(b"\x00\x00\x00\x00") == "0.0.0.0" 13 | assert unpack_ipv4(b"\xff\xff\xff\xff") == "255.255.255.255" 14 | assert unpack_ipv4(b"\x0a\x10\x20\x30") == "10.16.32.48" 15 | 16 | 17 | def test_unpack_ipv6(): 18 | assert ( 19 | unpack_ipv6(b"\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff") 20 | == "11:2233:4455:6677:8899:aabb:ccdd:eeff" 21 | ) 22 | 23 | 24 | def test_unpack_macaddr(): 25 | assert unpack_macaddr(b"\x00\x11\x22\xaa\xbb\xcc") == "00:11:22:aa:bb:cc" 26 | 27 | 28 | def test_unpack_euiaddr(): 29 | assert ( 30 | unpack_euiaddr(b"\x00\x11\x22\x33\xaa\xbb\xcc\xdd") == "00:11:22:33:aa:bb:cc:dd" 31 | ) 32 | 33 | 34 | def test_unpack_tsresol(): 35 | assert unpack_timestamp_resolution(bytes((0,))) == 1 36 | assert unpack_timestamp_resolution(bytes((1,))) == 1e-1 37 | assert unpack_timestamp_resolution(bytes((6,))) == 1e-6 38 | assert unpack_timestamp_resolution(bytes((100,))) == 1e-100 39 | 40 | assert unpack_timestamp_resolution(bytes((0 | 0b10000000,))) == 1 41 | assert unpack_timestamp_resolution(bytes((1 | 0b10000000,))) == 2**-1 42 | assert unpack_timestamp_resolution(bytes((6 | 0b10000000,))) == 2**-6 43 | assert unpack_timestamp_resolution(bytes((100 | 0b10000000,))) == 2**-100 44 | 45 | 46 | def test_pack_tsresol(): 47 | assert pack_timestamp_resolution(10, 0b00000000) == bytes((0b00000000,)) 48 | assert pack_timestamp_resolution(10, 0b00000011) == bytes((0b00000011,)) 49 | assert pack_timestamp_resolution(10, 0b00000100) == bytes((0b00000100,)) 50 | assert pack_timestamp_resolution(10, 0b00111100) == bytes((0b00111100,)) 51 | 52 | assert pack_timestamp_resolution(2, 0b00000000) == bytes((0b10000000,)) 53 | assert pack_timestamp_resolution(2, 0b00000011) == bytes((0b10000011,)) 54 | assert pack_timestamp_resolution(2, 0b00000100) == bytes((0b10000100,)) 55 | assert pack_timestamp_resolution(2, 0b00111100) == bytes((0b10111100,)) 56 | -------------------------------------------------------------------------------- /tests/test_write_support.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import pytest 4 | 5 | import pcapng.blocks as blocks 6 | from pcapng import FileScanner, FileWriter 7 | from pcapng.exceptions import PcapngStrictnessError, PcapngStrictnessWarning 8 | from pcapng.strictness import Strictness, set_strictness 9 | 10 | 11 | def compare_blocklists(list1, list2): 12 | "Compare two lists of blocks. Helper for the below tests." 13 | 14 | assert len(list1) == len(list2) 15 | 16 | for i in range(0, len(list1)): 17 | assert list1[i] == list2[i], "block #{} mismatch".format(i) 18 | 19 | 20 | @pytest.mark.parametrize("endianness", ["<", ">"]) 21 | def test_write_read_all_blocks(endianness): 22 | # Track the blocks we're writing 23 | out_blocks = [] 24 | 25 | # Build our original/output session 26 | o_shb = blocks.SectionHeader( 27 | endianness=endianness, 28 | options={ 29 | "shb_hardware": "pytest", 30 | "shb_os": "python", 31 | "shb_userappl": "python-pcapng", 32 | }, 33 | ) 34 | out_blocks.append(o_shb) 35 | 36 | o_idb = o_shb.new_member( 37 | blocks.InterfaceDescription, 38 | link_type=1, 39 | snaplen=65535, 40 | options={ 41 | "if_name": "Interface Zero", 42 | "if_description": "Test interface", 43 | "if_os": "python", 44 | "if_hardware": "whatever", 45 | "if_filter": [(0, b"tcp port 23 and host 192.0.2.5")], 46 | }, 47 | ) 48 | out_blocks.append(o_idb) 49 | 50 | # The SHB and IDBs will be written right away here. 51 | fake_file = io.BytesIO() 52 | writer = FileWriter(fake_file, o_shb) 53 | 54 | # Add blocks to the output 55 | 56 | # epb 57 | blk = o_shb.new_member(blocks.EnhancedPacket) 58 | blk.packet_data = b"Test data 123 XYZ" 59 | writer.write_block(blk) 60 | out_blocks.append(blk) 61 | 62 | # spb 63 | blk = o_shb.new_member(blocks.SimplePacket) 64 | blk.packet_data = b"Test data 123 XYZ" 65 | writer.write_block(blk) 66 | out_blocks.append(blk) 67 | 68 | # pb (which is obsolete) 69 | set_strictness(Strictness.FORBID) 70 | blk = o_shb.new_member(blocks.ObsoletePacket) 71 | blk.packet_data = b"Test data 123 XYZ" 72 | with pytest.raises(PcapngStrictnessError, match="obsolete"): 73 | # Should prevent writing by default 74 | writer.write_block(blk) 75 | 76 | # Set to warning mode and try again 77 | set_strictness(Strictness.WARN) 78 | with pytest.warns(PcapngStrictnessWarning, match="obsolete"): 79 | # Should write the obsolete block now 80 | writer.write_block(blk) 81 | out_blocks.append(blk) 82 | 83 | # Set to fix mode and try again 84 | set_strictness(Strictness.FIX) 85 | with pytest.warns(PcapngStrictnessWarning, match="obsolete"): 86 | # Should write an enhanced block now 87 | writer.write_block(blk) 88 | out_blocks.append(blk.enhanced()) 89 | 90 | set_strictness(Strictness.FORBID) 91 | 92 | # nrb 93 | blk = o_shb.new_member( 94 | blocks.NameResolution, 95 | records=[ 96 | { 97 | "type": 1, 98 | "address": "127.0.0.1", 99 | "names": ["localhost", "localhost.localdomain"], 100 | }, 101 | { 102 | "type": 2, 103 | "address": "::1", 104 | "names": ["localhost", "localhost.localdomain"], 105 | }, 106 | ], 107 | ) 108 | writer.write_block(blk) 109 | out_blocks.append(blk) 110 | 111 | # isb 112 | blk = o_shb.new_member( 113 | blocks.InterfaceStatistics, 114 | interface_id=0, 115 | timestamp_high=0x01234567, 116 | timestamp_low=0x89ABCDEF, 117 | options={ 118 | "isb_starttime": 0x0123456789ABCD00, 119 | "isb_endtime": 0x0123456789ABCDEF, 120 | "isb_usrdeliv": 50, 121 | }, 122 | ) 123 | writer.write_block(blk) 124 | out_blocks.append(blk) 125 | 126 | # Done writing blocks. 127 | # Now get back what we wrote and see if things line up. 128 | fake_file.seek(0) 129 | in_blocks = list(FileScanner(fake_file)) 130 | 131 | compare_blocklists(in_blocks, out_blocks) 132 | 133 | 134 | @pytest.mark.parametrize("endianness", ["<", ">"]) 135 | def test_spb_snap_lengths(endianness): 136 | """ 137 | Simple Packet Blocks present a unique challenge in parsing. The packet does not 138 | contain an explicit "captured length" indicator, only the original observed 139 | packet length; one must consult the capturing network interface's snap length 140 | in order to determine whether the packet may have been truncated. 141 | 142 | The block interface was designed to take care of most of this for the developer, 143 | both for reading and writing. For reading, the :py:method:`captured_len` is 144 | a property that works out its value from the capturing interface and the original 145 | packet length. For writing, packet data will be truncated to the capturing 146 | interface's snap length if it would be too big. 147 | """ 148 | 149 | # Binary data to write/test 150 | data = bytes(range(0, 256)) 151 | 152 | # First session: no snap length 153 | o_shb = blocks.SectionHeader(endianness=endianness) 154 | o_idb = o_shb.new_member(blocks.InterfaceDescription) # noqa: F841 155 | o_blk1 = o_shb.new_member(blocks.SimplePacket, packet_data=data) 156 | 157 | fake_file = io.BytesIO() 158 | writer = FileWriter(fake_file, o_shb) 159 | writer.write_block(o_blk1) 160 | 161 | fake_file.seek(0) 162 | (i_shb, i_idb, i_blk1) = list(FileScanner(fake_file)) 163 | assert i_blk1.captured_len == len(data) 164 | assert i_blk1.packet_len == len(data) 165 | assert i_blk1.packet_data == data 166 | 167 | # Second session: with snap length 168 | o_shb = blocks.SectionHeader(endianness=endianness) 169 | o_idb = o_shb.new_member(blocks.InterfaceDescription, snaplen=32) # noqa: F841 170 | o_blk1 = o_shb.new_member(blocks.SimplePacket, packet_data=data[:16]) 171 | o_blk2 = o_shb.new_member(blocks.SimplePacket, packet_data=data[:32]) 172 | o_blk3 = o_shb.new_member(blocks.SimplePacket, packet_data=data[:33]) 173 | 174 | fake_file = io.BytesIO() 175 | writer = FileWriter(fake_file, o_shb) 176 | writer.write_block(o_blk1) 177 | writer.write_block(o_blk2) 178 | writer.write_block(o_blk3) 179 | 180 | fake_file.seek(0) 181 | (i_shb, i_idb, i_blk1, i_blk2, i_blk3) = list(FileScanner(fake_file)) 182 | 183 | assert i_blk1.captured_len == 16 184 | assert i_blk1.packet_len == 16 185 | assert i_blk1.packet_data == data[:16] 186 | 187 | assert i_blk2.captured_len == 32 188 | assert i_blk2.packet_len == 32 189 | assert i_blk2.packet_data == data[:32] 190 | 191 | assert i_blk3.captured_len == 32 192 | assert i_blk3.packet_len == 33 193 | assert i_blk3.packet_data == data[:32] 194 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35 3 | 4 | [testenv] 5 | deps = 6 | setuptools>=5.4.1 7 | pytest>=2.6.0 8 | # scapy==2.2.0-dev 9 | commands = 10 | py.test -vvv ./tests 11 | 12 | [testenv:coverage] 13 | basepython=python2.7 14 | deps = 15 | setuptools>=5.4.1 16 | pytest>=2.6.0 17 | pytest-cov>=1.8.0 18 | coverage>=3.7.0 19 | scapy==2.2.0-dev 20 | commands = 21 | py.test -vvv --cov=./pcapng --cov-report=term-missing ./tests 22 | --------------------------------------------------------------------------------