diff --git a/.config/ci/check_commits.sh b/.config/ci/check_commits.sh new file mode 100755 index 00000000000..c139e29ca77 --- /dev/null +++ b/.config/ci/check_commits.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-2.0-only + +# Check all commits in the PR have the "AI-Assisted" tag +# We copy Wireshark's contributing guide, thanks to them for the idea ! +# This script is inspired by https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml + +commits=$(git rev-list --no-merges --max-count=$((PR_FETCH_DEPTH - 1)) HEAD) +if [ -z "$commits" ]; then + echo "No commit to check in PR. OK." + exit 0 +fi + +missing=0 +for c in $commits; do + if ! git log -1 --format=%B "$c" | grep -qi '^AI-Assisted:'; then + echo -e "ERROR: Commit \033[0;33m$c\033[0m is missing the 'AI-Assisted: yes|no [tool(s)]' trailer." + missing=1 + else + echo -e "OK: Commit \033[0;32m$c\033[0m is properly tagged." + fi +done + +if [ $missing -eq 1 ]; then + echo + echo -e "\033[0;31mPlease add the 'AI-Assisted' trailer to commit messages !\033[0m" + echo "See the contribution guide at: https://github.com/secdev/scapy/blob/master/CONTRIBUTING.md" + exit 1 +else + echo "All checked commits include the AI-Assisted trailer." + exit 0 +fi diff --git a/.config/ci/test.ps1 b/.config/ci/test.ps1 index 0e6fc39c87c..5c6071bd4b2 100644 --- a/.config/ci/test.ps1 +++ b/.config/ci/test.ps1 @@ -6,7 +6,7 @@ # Usage: # ./test.ps1 # Examples: -# ./test.sh 3.13 +# ./test.sh 3.14 if ($args.Count -eq 0) { Write-Host "Usage: .\test.ps1 " diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index 7ed3f0caa78..5ed4ca8ee2d 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -7,6 +7,7 @@ ba browseable byteorder cace +cantact cas ciph componet diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a7340ee6c6b..51516b2c6af 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,25 @@ - + + +### Description - diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 5e8873e3142..81366c6cc86 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -16,14 +16,40 @@ permissions: contents: read jobs: + commit: + name: Check the validity of the commits + runs-on: ubuntu-latest + # We follow the same contributing patterns as Wireshark. Thanks to + # https://gitlab.com/wireshark/wireshark/-/blob/master/.gitlab-ci.yml + steps: + - name: Get the number of commits in the PR + run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}" + if: github.event_name == 'pull_request' + - name: Checkout Scapy + uses: actions/checkout@v6 + with: + fetch-depth: ${{ env.PR_FETCH_DEPTH }} + if: github.event_name == 'pull_request' + - name: AI trailer reminder + run: bash ./.config/ci/check_commits.sh + if: github.event_name == 'pull_request' + spdx: + name: Check SPDX identifiers + runs-on: ubuntu-latest + steps: + - name: Checkout Scapy + uses: actions/checkout@v6 + - name: All files should have a SPDX identifier + run: bash scapy/tools/check_spdx.sh health: name: Code health check runs-on: ubuntu-latest + needs: [commit, spdx] steps: - name: Checkout Scapy - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install tox @@ -39,34 +65,28 @@ jobs: docs: # 'runs-on' and 'python-version' should match the ones defined in .readthedocs.yml name: Build doc - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 + needs: [commit, spdx] steps: - name: Checkout Scapy - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - name: Install tox run: pip install tox - name: Build docs run: tox -e docs - spdx: - name: Check SPDX identifiers - runs-on: ubuntu-latest - steps: - - name: Checkout Scapy - uses: actions/checkout@v4 - - name: Launch script - run: bash scapy/tools/check_spdx.sh mypy: name: Type hints check runs-on: ubuntu-latest + needs: [commit, spdx] steps: - name: Checkout Scapy - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install tox @@ -77,13 +97,14 @@ jobs: utscapy: name: ${{ matrix.os }} ${{ matrix.installmode }} ${{ matrix.python }} ${{ matrix.mode }} ${{ matrix.flags }} runs-on: ${{ matrix.os }} + needs: [commit, spdx] timeout-minutes: 20 continue-on-error: ${{ matrix.allow-failure == 'true' }} strategy: fail-fast: false matrix: os: [ubuntu-latest] - python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python: ["3.8", "3.9", "3.10", "3.11", "3.13"] mode: [non_root] installmode: [''] flags: [" -K scanner"] @@ -96,7 +117,7 @@ jobs: flags: " -K scanner" # Linux root tests on last version - os: ubuntu-latest - python: "3.13" + python: "3.14" mode: root flags: " -K scanner" # PyPy tests: root only @@ -106,23 +127,23 @@ jobs: flags: " -K scanner" # Libpcap test - os: ubuntu-latest - python: "3.13" + python: "3.14" mode: root installmode: 'libpcap' flags: " -K scanner" # macOS tests - os: macos-14 - python: "3.13" + python: "3.14" mode: both flags: " -K scanner" # windows tests - os: windows-latest - python: "3.13" + python: "3.14" mode: root flags: " -K scanner" # Scanner tests - os: ubuntu-latest - python: "3.13" + python: "3.14" mode: root allow-failure: 'true' flags: " -k scanner" @@ -131,23 +152,23 @@ jobs: mode: root flags: " -k scanner" - os: macos-14 - python: "3.13" + python: "3.14" mode: both allow-failure: 'true' flags: " -k scanner" - os: windows-latest - python: "3.13" + python: "3.14" mode: both allow-failure: 'true' flags: " -k scanner" steps: - name: Checkout Scapy - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Codecov requires a fetch-depth > 1 with: fetch-depth: 2 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - name: Install Tox and any other packages (linux/osx) @@ -175,13 +196,14 @@ jobs: cryptography: name: pyca/cryptography test runs-on: ubuntu-latest + needs: [commit, spdx] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - name: Install tox run: pip install tox # pyca/cryptography's CI installs cryptography @@ -195,16 +217,17 @@ jobs: analyze: name: CodeQL analysis runs-on: ubuntu-latest + needs: [commit, spdx] permissions: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: 'python' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 diff --git a/.packit.yml b/.packit.yml index fa7bda9855b..a9f7ea30a6f 100644 --- a/.packit.yml +++ b/.packit.yml @@ -19,6 +19,7 @@ actions: - "rm -fv .packit_rpm/sources" # Drop all downstream patches to prevent them from interfering with upstream builds - "sed -ri '/^Patch[0-9]+\\:.+\\.patch/d' .packit_rpm/scapy.spec" + - "sed -i '/^%pyproject_check_import/d' .packit_rpm/scapy.spec" - "sed -i '/^%check$/apip3 install scapy-rpc\\nOPENSSL_ENABLE_SHA1_SIGNATURES=1 OPENSSL_CONF=$(python3 ./.config/ci/openssl.py) ./test/run_tests -c test/configs/linux.utsc -K ci_only -K netaccess -K scanner' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: can-utils' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: libpcap' .packit_rpm/scapy.spec" @@ -28,6 +29,7 @@ actions: - "sed -i '/^BuildArch/aBuildRequires: python3-ipython' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: python3-brotli' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: python3-can' .packit_rpm/scapy.spec" + - "sed -i '/^BuildArch/aBuildRequires: python3-cbor2' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: python3-coverage' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: python3-cryptography' .packit_rpm/scapy.spec" - "sed -i '/^BuildArch/aBuildRequires: python3-tkinter' .packit_rpm/scapy.spec" diff --git a/.readthedocs.yml b/.readthedocs.yml index b4732b29e04..95d3e60a559 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,9 +12,9 @@ formats: - pdf build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.12" + python: "3.14" # To show the correct Scapy version, we must unshallow # https://docs.readthedocs.io/en/stable/build-customization.html#unshallow-git-clone jobs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c1efcd9064..13879cf58da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,14 @@ submitting an issue. If you're not sure whether a behavior is a bug or not, submit an issue and ask, don't be shy! +### AI-assisted reports + + + + +If you use AI tools to help find or draft a bug report, please mention that and make sure you have personally verified the steps and details before submitting. +Purely AI-generated reports are not supported and might be closed; a quick human check keeps triage efficient for everyone. + ### Enhancements / feature requests If you want a feature in Scapy, but cannot implement it yourself or @@ -53,6 +61,18 @@ of function calls, packet creations, etc.). ### Coding style & conventions +- All commits should include the `AI-Assisted: (yes/no) [tool]` tag. This is used to disclose the AI tools that are used when authoring. You must check the commits you produce, or your PR might be closed. The tag may look like such: + + ``` + AI-Assisted: yes (Claude Opus 4.7) + ``` + or + + ``` + AI-Assisted: no + ``` + This guideline is adapted with thanks to [Wireshark's AI usage statement](https://www.wireshark.org/docs/wsdg_html_chunked/ChSrcContribute.html). + - The code should be PEP-8 compliant; you can check your code with [pep8](https://pypi.python.org/pypi/pep8) and the command `tox -e flake8` @@ -63,20 +83,18 @@ of function calls, packet creations, etc.). - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) is a nice read! -- Avoid creating unnecessary `list` objects, particularly if they - can be huge (e.g., when possible, use `for line in fdesc` instead of - `for line in fdesc.readlines()`; more generally prefer generators over - lists). - ### Tests -Please consider adding tests for your new features or that trigger the -bug you are fixing. This will prevent a regression from being -unnoticed. Do not use the variable `_` in your tests, as it could break them. +We require adding tests for all new features or bug fixes, or a justification as to why they are not relevant. We know it's annoying, but Scapy is all about parsing and dissecting weird protocols us maintainers will never encounter. Having good tests is the only way to keep the code maintainable. + +- If you are fixing a bug, provide a one-liner that reproduced the bug you are fixing. +- If you are introducing dissectors, provide at least a very simple "dissect" / "build" of real packets with simple assertions. +- Tests can be very simple. It's much better to have dumb tests that break when one does changes than no tests. +- Do not use the variable `_` in your tests, as it could break them. If you find yourself in a situation where your tests locally succeed but fail if executed on the CI, try to enable the debugging option for the -dissector by setting `conf.debug_dissector = 1`. +dissector by setting `conf.debug_dissector = 1`. In doubt, feel free to ask maintainers for help. ### New protocols diff --git a/README.md b/README.md index 0a9b17ae4b5..559b8ee5596 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,11 @@ follow the instructions to install them. +## Packaging status + +[![Packaging status](https://repology.org/badge/vertical-allrepos/scapy.svg?columns=4&exclude_unsupported=1&header= +)](https://repology.org/project/scapy/versions) + ## License Scapy's code, tests and tools are licensed under GPL v2. diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index 695b0403a77..17995cfb6b5 100644 --- a/doc/scapy/installation.rst +++ b/doc/scapy/installation.rst @@ -29,7 +29,7 @@ Scapy versions +------------------+-------+-------+--------+ | Python 3.4-3.6 | ❌ | ✅ | ❌ | +------------------+-------+-------+--------+ -| Python 3.7-3.11 | ❌ | ✅ | ✅ | +| Python 3.7-3.14 | ❌ | ✅ | ✅ | +------------------+-------+-------+--------+ Installing Scapy v2.x diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 6abee5daa66..a9fa9d3e107 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1081,6 +1081,98 @@ to the Scapy interpreter:: .. image:: ../graphics/animations/animation-scapy-uds3.svg + +Single Layer Mode +----------------- + +UDS, KWP, OBD, and GMLAN all support a *single layer mode* that makes each +service packet a standalone ``Packet`` rather than a nested sublayer. + +**Default (multi-layer) mode** + +.. code-block:: python + + >>> pkt = UDS() / UDS_DSC(diagnosticSessionType=0x01) + >>> UDS(b'\x10\x01') + > + +**Single layer mode** + +To enable before loading a module:: + + >>> conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + ... 'single_layer_mode': True} + >>> load_contrib('automotive.uds') + +To toggle at runtime after loading:: + + >>> conf.contribs['UDS']['single_layer_mode'] = True + >>> UDS(b'\x10\x01') + + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + >>> conf.contribs['UDS']['single_layer_mode'] = False # revert to multi-layer mode + +The same ``single_layer_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. + +Compatibility Mode +------------------ + +Scapy allows crafting packets freely, including stacking a service sub-packet +on top of the base protocol layer (e.g. ``UDS()/UDS_DSC()``). When both +``single_layer_mode`` *and* stacking are used together, the ``service`` byte +would normally appear twice in the resulting byte stream – once from the base +layer and once from the sub-packet's own ``service`` ConditionalField. + +The **compatibility mode** flag (``compatibility_mode``, default ``True``) +addresses this: when it is enabled and ``single_layer_mode`` is active, the +sub-packet's ``service`` field is automatically **suppressed** whenever the +immediate underlayer is already the matching base-protocol packet. + +.. list-table:: Behaviour matrix + :header-rows: 1 + :widths: 25 25 50 + + * - ``single_layer_mode`` + - ``compatibility_mode`` + - ``UDS()/UDS_DSC()`` byte layout + * - ``False`` + - any + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) + * - ``True`` + - ``True`` *(default)* + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) — duplicate suppressed + * - ``True`` + - ``False`` + - ``service`` (UDS) + ``service`` (UDS_DSC) + ``diagnosticSessionType`` (UDS_DSC) + +Example with compatibility mode on (default):: + + >>> conf.contribs['UDS']['single_layer_mode'] = True + >>> conf.contribs['UDS']['compatibility_mode'] = True # already the default + + >>> # Standalone sub-packet: service field IS present (no UDS underlayer) + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + + >>> # Stacked: service field in UDS_DSC is suppressed (UDS is the underlayer) + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + +Example with compatibility mode off:: + + >>> conf.contribs['UDS']['compatibility_mode'] = False + + >>> # Stacked: both UDS and UDS_DSC emit a service byte + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x10\x01' + + >>> conf.contribs['UDS']['compatibility_mode'] = True # restore default + +The same ``compatibility_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. + GMLAN ===== diff --git a/doc/scapy/layers/gssapi.rst b/doc/scapy/layers/gssapi.rst index d4b3d6571bd..6322d2750c5 100644 --- a/doc/scapy/layers/gssapi.rst +++ b/doc/scapy/layers/gssapi.rst @@ -21,6 +21,7 @@ The following SSPs are currently provided: - :class:`~scapy.layers.kerberos.KerberosSSP` - :class:`~scapy.layers.spnego.SPNEGOSSP` - :class:`~scapy.layers.msrpce.msnrpc.NetlogonSSP` + - :class:`~scapy.arch.windows.sspi.WinSSP` (Windows only) Basically those are classes that implement two functions, trying to micmic the RFCs: @@ -134,4 +135,24 @@ Let's use :class:`~scapy.layers.ntlm.NTLMSSP` as an example of server-side SSP. } ) -You'll find other examples of how to instantiate a SSP in the docstrings of each SSP. See `the list <#ssplist>`_ \ No newline at end of file +You'll find other examples of how to instantiate a SSP in the docstrings of each SSP. See `the list <#ssplist>`_ + +WinSSP +~~~~~~ + +WinSSP is a special SSP that is only available on Windows, which calls the actual Windows SSPs local to the machine it's running on. +It allows to use the implicit authentication of the logged-in user with Scapy and its various clients, and is also sometimes necessary if you get unexpected ACCESS_DENIED on loopback connections. + +For instance using SPNEGO: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="Negotiate") + +For instance using NTLM: + +.. code:: python + + from scapy.arch.windows.sspi import * + clissp = WinSSP(Package="NTLM") diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index a2cfe35ee42..f27577da5fd 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -76,9 +76,9 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> load_module("ticketer") >>> t = Ticketer() >>> # If P12: - >>> t.request_tgt("Administrator@DOMAIN.LOCAL", p12="admin.pfx", ca="ca.pem") + >>> t.request_tgt(p12="admin.pfx", realm="DOMAIN.LOCAL", ca="ca.pem") >>> # One could also have used a different cert and key file: - >>> t.request_tgt("Administrator@DOMAIN.LOCAL", x509="admin.cert", x509key="admin.key", ca="ca.pem") + >>> t.request_tgt(x509="admin.cert", x509key="admin.key", realm="DOMAIN.LOCAL", ca="ca.pem") - **Request a user TGT with Kerberos armoring (FAST)** diff --git a/pyproject.toml b/pyproject.toml index 1d5ffabfc4f..805ba278b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Security", "Topic :: System :: Networking", "Topic :: System :: Networking :: Monitoring", diff --git a/scapy/__init__.py b/scapy/__init__.py index 3eb172ec490..139ba7a9371 100644 --- a/scapy/__init__.py +++ b/scapy/__init__.py @@ -30,12 +30,12 @@ def _parse_tag(tag): Example:: - v2.3.2-346-g164a52c075c8 -> '2.3.2.dev346' + v2.3.2-346-g164a52c075c8 -> '2.3.2.post346' """ match = re.match('^v?(.+?)-(\\d+)-g[a-f0-9]+$', tag) if match: - # remove the 'v' prefix and add a '.devN' suffix - return '%s.dev%s' % (match.group(1), match.group(2)) + # remove the 'v' prefix and add a '.postN' suffix + return '%s.post%s' % (match.group(1), match.group(2)) else: match = re.match('^v?([\\d\\.]+(rc\\d+)?)$', tag) if match: @@ -93,13 +93,13 @@ def _version_from_git_describe(): The tag prefix (``v``) and the git commit sha1 (``-g164a52c075c8``) are removed if present. - If the current directory is not exactly on the tag, a ``.devN`` suffix is + If the current directory is not exactly on the tag, a ``.postN`` suffix is appended where N is the number of commits made after the last tag. Example:: >>> _version_from_git_describe() - '2.3.2.dev346' + '2.3.2.post346' :raises CalledProcessError: if git is unavailable :return: Scapy's latest tag diff --git a/scapy/arch/__init__.py b/scapy/arch/__init__.py index 316d398f570..bea0a570e02 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -143,6 +143,7 @@ def get_if_raw_addr6(iff): elif WINDOWS: from scapy.arch.windows import * # noqa F403 from scapy.arch.windows.native import * # noqa F403 + from scapy.arch.windows.sspi import * # noqa F403 SIOCGIFHWADDR = 0 # mypy compat else: log_loading.critical( diff --git a/scapy/arch/windows/sspi.py b/scapy/arch/windows/sspi.py new file mode 100644 index 00000000000..8567d534d80 --- /dev/null +++ b/scapy/arch/windows/sspi.py @@ -0,0 +1,1000 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +SSP for implicit authentication on Windows +""" + +import ctypes +import ctypes.wintypes +import enum + +from scapy.layers.gssapi import ( + GSS_C_FLAGS, + GSS_S_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GssChannelBindings, + GSSAPI_BLOB, + GSSAPI_BLOB_SIGNATURE, + SSP, + GSS_S_BAD_NAME, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_CREDENTIAL, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FAILURE, + GSS_S_UNAUTHORIZED, + GSS_S_UNAVAILABLE, +) + +# Typing imports +from typing import ( + Optional, + List, +) + +# Windows bindings + +SECPKG_CRED_INBOUND = 0x00000001 +SECPKG_CRED_OUTBOUND = 0x00000002 +SECPKG_CRED_BOTH = 0x00000003 + +SECPKG_ATTR_SIZES = 0 +SECPKG_ATTR_SESSION_KEY = 9 +SECPKG_ATTR_SERVER_FLAGS = 14 + + +class SecPkgContext_SessionKey(ctypes.Structure): + _fields_ = [ + ("SessionKeyLength", ctypes.wintypes.ULONG), + ("SessionKey", ctypes.wintypes.LPBYTE), + ] + + +class SecPkgContext_Flags(ctypes.Structure): + _fields_ = [ + ("Flags", ctypes.wintypes.ULONG), + ] + + +class SecPkgContext_Sizes(ctypes.Structure): + _fields_ = [ + ("cbMaxToken", ctypes.wintypes.ULONG), + ("cbMaxSignature", ctypes.wintypes.ULONG), + ("cbBlockSize", ctypes.wintypes.ULONG), + ("cbSecurityTrailer", ctypes.wintypes.ULONG), + ] + + +class SEC_CHANNEL_BINDINGS(ctypes.Structure): + _fields_ = [ + ("dwInitiatorAddrType", ctypes.wintypes.ULONG), + ("cbInitiatorLength", ctypes.wintypes.ULONG), + ("dwInitiatorOffset", ctypes.wintypes.ULONG), + ("dwAcceptorAddrType", ctypes.wintypes.ULONG), + ("cbAcceptorLength", ctypes.wintypes.ULONG), + ("dwAcceptorOffset", ctypes.wintypes.ULONG), + ("cbApplicationDataLength", ctypes.wintypes.ULONG), + ("dwApplicationDataOffset", ctypes.wintypes.ULONG), + ] + + @classmethod + def from_GSS(cls, bindings: GssChannelBindings): + """ + Convert a GssChannelBindings to SecPkgContext_Bindings + """ + # Initialize structure + buffer = ctypes.create_string_buffer( + ctypes.sizeof(SEC_CHANNEL_BINDINGS) + + len(bindings.initiator_address.value) + + len(bindings.acceptor_address.value) + + len(bindings.application_data.value) + ) + Bindings = ctypes.cast( + ctypes.byref(buffer), + ctypes.POINTER(SEC_CHANNEL_BINDINGS), + ) + + # Populate values with the offsets and lengths + offset = ctypes.sizeof(SEC_CHANNEL_BINDINGS) + Bindings.contents.dwInitiatorAddrType = bindings.initiator_addrtype + if bindings.initiator_address.value: + lgth = len(bindings.initiator_address.value) + Bindings.contents.cbInitiatorLength = lgth + Bindings.contents.dwInitiatorOffset = offset + buffer[offset : offset + lgth] = bindings.initiator_address.value + offset += lgth + Bindings.contents.dwAcceptorAddrType = bindings.acceptor_addrtype + if bindings.acceptor_address.value: + lgth = len(bindings.acceptor_address.value) + Bindings.contents.cbAcceptorLength = lgth + Bindings.contents.dwAcceptorOffset = offset + buffer[offset : offset + lgth] = bindings.acceptor_address.value + offset += lgth + if bindings.application_data.value: + lgth = len(bindings.application_data.value) + Bindings.contents.cbApplicationDataLength = lgth + Bindings.contents.dwApplicationDataOffset = offset + buffer[offset : offset + lgth] = bindings.application_data.value + offset += lgth + + return buffer, offset + + +SECURITY_NETWORK_DREP = 0 + + +class SEC_CODES(enum.IntEnum): + """ + Windows sspi.h return codes + """ + + SEC_E_OK = 0x00000000 + SEC_I_CONTINUE_NEEDED = 0x00090312 + SEC_I_COMPLETE_AND_CONTINUE = 0x00090314 + SEC_E_INSUFFICIENT_MEMORY = 0x80090300 + SEC_E_INTERNAL_ERROR = 0x80090304 + SEC_E_INVALID_HANDLE = 0x80090301 + SEC_E_INVALID_TOKEN = 0x80090308 + SEC_E_LOGON_DENIED = 0x8009030C + SEC_E_NO_AUTHENTICATING_AUTHORITY = 0x80090311 + SEC_E_NO_CREDENTIALS = 0x8009030E + SEC_E_TARGET_UNKNOWN = 0x80090303 + SEC_E_UNSUPPORTED_FUNCTION = 0x80090302 + SEC_E_WRONG_PRINCIPAL = 0x80090322 + + @staticmethod + def to_GSS(code: int): + if code in _GSS_REG_TRANSLATION: + return _GSS_REG_TRANSLATION[code] + else: + return code + + +_GSS_REG_TRANSLATION = { + SEC_CODES.SEC_E_OK: GSS_S_COMPLETE, + SEC_CODES.SEC_I_CONTINUE_NEEDED: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE: GSS_S_CONTINUE_NEEDED, + SEC_CODES.SEC_E_INSUFFICIENT_MEMORY: GSS_S_FAILURE, + SEC_CODES.SEC_E_INTERNAL_ERROR: GSS_S_FAILURE, + SEC_CODES.SEC_E_INVALID_HANDLE: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_INVALID_TOKEN: GSS_S_DEFECTIVE_TOKEN, + SEC_CODES.SEC_E_LOGON_DENIED: GSS_S_UNAUTHORIZED, + SEC_CODES.SEC_E_NO_AUTHENTICATING_AUTHORITY: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_NO_CREDENTIALS: GSS_S_DEFECTIVE_CREDENTIAL, + SEC_CODES.SEC_E_TARGET_UNKNOWN: GSS_S_BAD_NAME, + SEC_CODES.SEC_E_UNSUPPORTED_FUNCTION: GSS_S_UNAVAILABLE, + SEC_CODES.SEC_E_WRONG_PRINCIPAL: GSS_S_BAD_NAME, +} + + +class SECURITY_INTEGER(ctypes.Structure): + _fields_ = [ + ("LowPart", ctypes.wintypes.ULONG), + ("HighPart", ctypes.wintypes.LONG), + ] + + +class SecHandle(ctypes.Structure): + _fields_ = [ + ("dwLower", ctypes.POINTER(ctypes.wintypes.ULONG)), + ("dwUpper", ctypes.POINTER(ctypes.wintypes.ULONG)), + ] + + +_winapi_AcquireCredentialsHandle = ctypes.windll.secur32.AcquireCredentialsHandleW +_winapi_AcquireCredentialsHandle.restype = ctypes.wintypes.DWORD +_winapi_AcquireCredentialsHandle.argtypes = [ + ctypes.wintypes.LPWSTR, # pszPrincipal + ctypes.wintypes.LPWSTR, # pszPackage + ctypes.wintypes.ULONG, # fCredentialUse + ctypes.c_void_p, # pvLogonID + ctypes.c_void_p, # pAuthData + ctypes.c_void_p, # pGetKeyFn + ctypes.c_void_p, # pvGetKeyArgument + ctypes.POINTER(SecHandle), # phCredential, + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + + +class SecBuffer(ctypes.Structure): + _fields_ = [ + ("cbBuffer", ctypes.wintypes.ULONG), + ("BufferType", ctypes.wintypes.ULONG), + ("pvBuffer", ctypes.c_void_p), + ] + + def GetData(self): + if self.cbBuffer == 0: + return b"" + buf = ctypes.cast( + self.pvBuffer, + ctypes.POINTER(ctypes.wintypes.BYTE * self.cbBuffer), + ) + return bytes(buf.contents) + + +SECBUFFER_VERSION = 0 +SECBUFFER_DATA = 1 +SECBUFFER_TOKEN = 2 +SECBUFFER_READONLY = 0x80000000 +SECBUFFER_CHANNEL_BINDINGS = 14 + + +class SecBufferDesc(ctypes.Structure): + _fields_ = [ + ("ulVersion", ctypes.wintypes.ULONG), + ("cBuffers", ctypes.wintypes.ULONG), + ("pBuffers", ctypes.POINTER(ctypes.POINTER(SecBuffer))), + ] + + @staticmethod + def Create(Buffers: List[SecBuffer]): + Buffers = ctypes.ARRAY(SecBuffer, len(Buffers))(*Buffers) + Output = SecBufferDesc( + SECBUFFER_VERSION, + len(Buffers), + ctypes.cast( + ctypes.byref(Buffers), ctypes.POINTER(ctypes.POINTER(SecBuffer)) + ), + ) + return Buffers, Output + + @staticmethod + def ParseBuffer(Buffers: ctypes.ARRAY, BufferType: int, cls): + for Buffer in Buffers: + if Buffer.BufferType == BufferType: + return cls(Buffer.GetData()) + return None + + +_winapi_InitializeSecurityContext = ctypes.windll.secur32.InitializeSecurityContextW +_winapi_InitializeSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_InitializeSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.wintypes.LPCWSTR, # pszTargetName + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # Reserved1 (must be 0) + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecBufferDesc), # pInput (can be NULL) + ctypes.wintypes.ULONG, # Reserved2 (must be 0) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_AcceptSecurityContext = ctypes.windll.secur32.AcceptSecurityContext +_winapi_AcceptSecurityContext.restype = ctypes.wintypes.DWORD +_winapi_AcceptSecurityContext.argtypes = [ + ctypes.POINTER(SecHandle), # phCredential + ctypes.POINTER(SecHandle), # phContext (NULL on first call) + ctypes.POINTER(SecBufferDesc), # pInput + ctypes.wintypes.ULONG, # fContextReq + ctypes.wintypes.ULONG, # TargetDataRep (e.g. SECURITY_NATIVE_DREP) + ctypes.POINTER(SecHandle), # phNewContext + ctypes.POINTER(SecBufferDesc), # pOutput + ctypes.POINTER(ctypes.wintypes.ULONG), # pfContextAttr + ctypes.POINTER(SECURITY_INTEGER), # ptsExpiry +] + +_winapi_MakeSignature = ctypes.windll.secur32.MakeSignature +_winapi_MakeSignature.restype = ctypes.wintypes.DWORD +_winapi_MakeSignature.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.wintypes.ULONG, # fQOP + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo +] + +_winapi_VerifySignature = ctypes.windll.secur32.VerifySignature +_winapi_VerifySignature.restype = ctypes.wintypes.DWORD +_winapi_VerifySignature.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_DecryptMessage = ctypes.windll.secur32.DecryptMessage +_winapi_DecryptMessage.restype = ctypes.wintypes.DWORD +_winapi_DecryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_EncryptMessage = ctypes.windll.secur32.EncryptMessage +_winapi_EncryptMessage.restype = ctypes.wintypes.DWORD +_winapi_EncryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.wintypes.ULONG, # fQOP + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo +] + +_winapi_DecryptMessage = ctypes.windll.secur32.DecryptMessage +_winapi_DecryptMessage.restype = ctypes.wintypes.DWORD +_winapi_DecryptMessage.argtypes = [ + ctypes.POINTER(SecHandle), # phContext + ctypes.POINTER(SecBufferDesc), # pMessage + ctypes.wintypes.ULONG, # MessageSeqNo + ctypes.POINTER(ctypes.wintypes.ULONG), # pfQOP +] + +_winapi_FreeContextBuffer = ctypes.windll.secur32.FreeContextBuffer +_winapi_FreeContextBuffer.restype = ctypes.wintypes.DWORD +_winapi_FreeContextBuffer.argtypes = [ctypes.c_void_p] + +_winapi_QueryContextAttributesW = ctypes.windll.secur32.QueryContextAttributesW +_winapi_QueryContextAttributesW.restype = ctypes.wintypes.DWORD +_winapi_QueryContextAttributesW.argtypes = [ + ctypes.POINTER(SecHandle), + ctypes.wintypes.ULONG, + ctypes.c_void_p, +] + +_winapi_SspiGetTargetHostName = ctypes.windll.secur32.SspiGetTargetHostName +_winapi_SspiGetTargetHostName.restype = ctypes.wintypes.DWORD +_winapi_SspiGetTargetHostName.argtypes = [ + ctypes.wintypes.LPCWSTR, + ctypes.POINTER(ctypes.wintypes.LPWSTR), +] + + +# Types + + +class ISC_REQ_FLAGS(enum.IntFlag): + """ + ISC_REQ Flags per sspi.h + """ + + ISC_REQ_DELEGATE = 0x00000001 + ISC_REQ_MUTUAL_AUTH = 0x00000002 + ISC_REQ_REPLAY_DETECT = 0x00000004 + ISC_REQ_SEQUENCE_DETECT = 0x00000008 + ISC_REQ_CONFIDENTIALITY = 0x00000010 + ISC_REQ_USE_SESSION_KEY = 0x00000020 + ISC_REQ_PROMPT_FOR_CREDS = 0x00000040 + ISC_REQ_USE_SUPPLIED_CREDS = 0x00000080 + ISC_REQ_ALLOCATE_MEMORY = 0x00000100 + ISC_REQ_USE_DCE_STYLE = 0x00000200 + ISC_REQ_DATAGRAM = 0x00000400 + ISC_REQ_CONNECTION = 0x00000800 + ISC_REQ_CALL_LEVEL = 0x00001000 + ISC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ISC_REQ_EXTENDED_ERROR = 0x00004000 + ISC_REQ_STREAM = 0x00008000 + ISC_REQ_INTEGRITY = 0x00010000 + ISC_REQ_IDENTIFY = 0x00020000 + ISC_REQ_NULL_SESSION = 0x00040000 + ISC_REQ_MANUAL_CRED_VALIDATION = 0x00080000 + ISC_REQ_RESERVED1 = 0x00100000 + ISC_REQ_FRAGMENT_TO_FIT = 0x00200000 + ISC_REQ_FORWARD_CREDENTIALS = 0x00400000 + ISC_REQ_NO_INTEGRITY = 0x00800000 + ISC_REQ_USE_HTTP_STYLE = 0x01000000 + ISC_REQ_UNVERIFIED_TARGET_NAME = 0x20000000 + ISC_REQ_CONFIDENTIALITY_ONLY = 0x40000000 + ISC_REQ_MESSAGES = 0x0000000100000000 + ISC_REQ_DEFERRED_CRED_VALIDATION = 0x0000000200000000 + ISC_REQ_NO_POST_HANDSHAKE_AUTH = 0x0000000400000000 + ISC_REQ_REUSE_SESSION_TICKETS = 0x0000000800000000 + ISC_REQ_EXPLICIT_SESSION = 0x0000001000000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ISC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ISC_REQ_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & gssf: + result |= iscf + return ISC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ISC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ISC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, iscf in _GSS_ISC_TRANSLATION.items(): + if flags & iscf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ISC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ISC_REQ_FLAGS.ISC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ISC_REQ_FLAGS.ISC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ISC_REQ_FLAGS.ISC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ISC_REQ_FLAGS.ISC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ISC_REQ_FLAGS.ISC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ISC_REQ_FLAGS.ISC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ISC_REQ_FLAGS.ISC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ISC_REQ_FLAGS.ISC_REQ_EXTENDED_ERROR, +} + + +class ASC_REQ_FLAGS(enum.IntFlag): + ASC_REQ_DELEGATE = 0x00000001 + ASC_REQ_MUTUAL_AUTH = 0x00000002 + ASC_REQ_REPLAY_DETECT = 0x00000004 + ASC_REQ_SEQUENCE_DETECT = 0x00000008 + ASC_REQ_CONFIDENTIALITY = 0x00000010 + ASC_REQ_USE_SESSION_KEY = 0x00000020 + ASC_REQ_SESSION_TICKET = 0x00000040 + ASC_REQ_ALLOCATE_MEMORY = 0x00000100 + ASC_REQ_USE_DCE_STYLE = 0x00000200 + ASC_REQ_DATAGRAM = 0x00000400 + ASC_REQ_CONNECTION = 0x00000800 + ASC_REQ_CALL_LEVEL = 0x00001000 + ASC_REQ_FRAGMENT_SUPPLIED = 0x00002000 + ASC_REQ_EXTENDED_ERROR = 0x00008000 + ASC_REQ_STREAM = 0x00010000 + ASC_REQ_INTEGRITY = 0x00020000 + ASC_REQ_LICENSING = 0x00040000 + ASC_REQ_IDENTIFY = 0x00080000 + ASC_REQ_ALLOW_NULL_SESSION = 0x00100000 + ASC_REQ_ALLOW_NON_USER_LOGONS = 0x00200000 + ASC_REQ_ALLOW_CONTEXT_REPLAY = 0x00400000 + ASC_REQ_FRAGMENT_TO_FIT = 0x00800000 + ASC_REQ_NO_TOKEN = 0x01000000 + ASC_REQ_PROXY_BINDINGS = 0x04000000 + ASC_REQ_ALLOW_MISSING_BINDINGS = 0x10000000 + + @staticmethod + def from_GSS(flags: GSS_C_FLAGS) -> "ASC_REQ_FLAGS": + """ + Convert GSS_C_FLAGS into ASC_REQ_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & gssf: + result |= ascf + return ASC_REQ_FLAGS(result) + + @staticmethod + def to_GSS(flags: "ASC_REQ_FLAGS") -> GSS_C_FLAGS: + """ + Convert ASC_REQ_FLAGS into GSS_C_FLAGS + """ + result = 0 + for gssf, ascf in _GSS_ASC_TRANSLATION.items(): + if flags & ascf: + result |= gssf + return GSS_C_FLAGS(result) + + +_GSS_ASC_TRANSLATION = { + GSS_C_FLAGS.GSS_C_DELEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_DELEGATE, + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG: ASC_REQ_FLAGS.ASC_REQ_MUTUAL_AUTH, + GSS_C_FLAGS.GSS_C_REPLAY_FLAG: ASC_REQ_FLAGS.ASC_REQ_REPLAY_DETECT, + GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG: ASC_REQ_FLAGS.ASC_REQ_SEQUENCE_DETECT, + GSS_C_FLAGS.GSS_C_CONF_FLAG: ASC_REQ_FLAGS.ASC_REQ_CONFIDENTIALITY, + GSS_C_FLAGS.GSS_C_INTEG_FLAG: ASC_REQ_FLAGS.ASC_REQ_INTEGRITY, + GSS_C_FLAGS.GSS_C_DCE_STYLE: ASC_REQ_FLAGS.ASC_REQ_USE_DCE_STYLE, + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG: ASC_REQ_FLAGS.ASC_REQ_IDENTIFY, + GSS_C_FLAGS.GSS_C_EXTENDED_ERROR_FLAG: ASC_REQ_FLAGS.ASC_REQ_EXTENDED_ERROR, + GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS: ASC_REQ_FLAGS.ASC_REQ_ALLOW_MISSING_BINDINGS, # noqa: E501 +} + + +# The SSP + + +class WinSSP(SSP): + """ + Use a native Windows SSP through SSPI + + :param Package: the SSP to use + """ + + class STATE(SSP.STATE): + NEGOTIATING = 1 + COMPLETED = 2 + + class CONTEXT(SSP.CONTEXT): + __slots__ = [ + "state", + "Credential", + "Package", + "phContext", + "ptsExpiry", + "SessionKey", + "ServerHostname", + "SendSeqNum", + "RecvSeqNum", + "cbMaxSignature", + "cbSecurityTrailer", + ] + + def __init__( + self, + Package: str, + CredentialUse: int, + req_flags: Optional["GSS_C_FLAGS | GSS_S_FLAGS"] = None, + ): + self.Credential = SecHandle() + self.phContext = None + self.ptsExpiry = SECURITY_INTEGER() + self.Package = Package + self.state = WinSSP.STATE.NEGOTIATING + self.ServerHostname = None + + status = _winapi_AcquireCredentialsHandle( + None, + Package, + CredentialUse, + None, + None, + None, + None, + ctypes.byref(self.Credential), + ctypes.byref(self.ptsExpiry), + ) + if status != SEC_CODES.SEC_E_OK: + raise OSError(f"AcquireCredentialsHandle failed: {hex(status)}") + + super(WinSSP.CONTEXT, self).__init__( + req_flags=req_flags, + ) + + def QuerySessionKey(self): + """ + Query the session key + """ + Buffer = SecPkgContext_SessionKey() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SESSION_KEY, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + SessionKeyBuf = ctypes.cast( + Buffer.SessionKey, + ctypes.POINTER(ctypes.wintypes.BYTE * Buffer.SessionKeyLength), + ) + self.SessionKey = bytes(SessionKeyBuf.contents) + + def QueryNegotiatedFlags(self): + """ + Query the negotiated flags. + """ + Buffer = SecPkgContext_Flags() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SERVER_FLAGS, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + self.flags = ISC_REQ_FLAGS.to_GSS(Buffer.Flags) + + def QueryPkgContextSizes(self): + """ + Query the package context sizes + """ + Buffer = SecPkgContext_Sizes() + + status = _winapi_QueryContextAttributesW( + self.phContext, + SECPKG_ATTR_SIZES, + ctypes.byref(Buffer), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"QueryContextAttributesW failed with: {hex(status)}") + + self.cbMaxSignature = Buffer.cbMaxSignature + self.cbSecurityTrailer = Buffer.cbSecurityTrailer + + def __repr__(self): + return "[Native SSP: %s]" % self.Package + + def __init__(self, Package: str = "Negotiate"): + self.Package = Package + if self.Package == "Negotiate": + self.auth_type = 0x09 + elif self.Package == "NTLM": + self.auth_type = 0x0A + elif self.Package == "Kerberos": + self.auth_type = 0x10 + super(WinSSP, self).__init__() + + def GSS_Init_sec_context( + self, + Context: CONTEXT, + input_token=None, + target_name: Optional[str] = None, + req_flags: Optional[GSS_C_FLAGS] = None, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_OUTBOUND, + req_flags=req_flags, + ) + + if Context.state == self.STATE.COMPLETED: + # SSPI and GSSAPI count completion differently, so we might + # be called one time for nothing. Return that we completed properly. + return Context, None, GSS_S_COMPLETE + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + chan_bindings, lgth = SEC_CHANNEL_BINDINGS.from_GSS(chan_bindings) + InputBuffers.append( + SecBuffer( + lgth, + SECBUFFER_CHANNEL_BINDINGS, + ctypes.cast(chan_bindings, ctypes.c_void_p), + ) + ) + if InputBuffers: + InputBuffers, Input = SecBufferDesc.Create(InputBuffers) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers, Output = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + if target_name: + TargetName = ctypes.cast( + ctypes.create_string_buffer( + target_name.encode("utf-16le") + b"\x00\x00" + ), + ctypes.wintypes.LPCWSTR, + ) + + HostName = ctypes.wintypes.LPWSTR() + status = _winapi_SspiGetTargetHostName(TargetName, ctypes.byref(HostName)) + if status == SEC_CODES.SEC_E_OK: + Context.ServerHostname = HostName.value + else: + TargetName = None + + # Call SSPI + status = _winapi_InitializeSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + TargetName, + ISC_REQ_FLAGS.from_GSS(Context.flags) + | ISC_REQ_FLAGS.ISC_REQ_ALLOCATE_MEMORY, + 0, + SECURITY_NETWORK_DREP, + Input and ctypes.byref(Input), + 0, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + # Extract output token + output_token = SecBufferDesc.ParseBuffer( + OutputBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB + ) + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.QueryPkgContextSizes() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) + + def GSS_Accept_sec_context( + self, + Context: CONTEXT, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, + chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + ): + # Get context + if not Context: + Context = self.CONTEXT( + self.Package, + SECPKG_CRED_INBOUND, + req_flags=req_flags, + ) + + # Create and populate the input buffers + InputBuffers = [] + if input_token: + input_token = bytes(input_token) + InputBuffers.append( + SecBuffer( + len(input_token), + SECBUFFER_TOKEN, + ctypes.cast( + ctypes.create_string_buffer(input_token), ctypes.c_void_p + ), + ) + ) + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: + chan_bindings, lgth = SEC_CHANNEL_BINDINGS.from_GSS(chan_bindings) + InputBuffers.append( + SecBuffer( + lgth, + SECBUFFER_CHANNEL_BINDINGS, + ctypes.cast(chan_bindings, ctypes.c_void_p), + ) + ) + if InputBuffers: + InputBuffers, Input = SecBufferDesc.Create(InputBuffers) + else: + Input = None + + # Create the output buffers (empty for now) + OutputBuffers, Output = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(0), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.c_void_p(), + ) + ] + ) + + # Prepare other arguments + phNewContext = Context.phContext or SecHandle() + pfContextAttr = ctypes.wintypes.ULONG() + + # Call SSPI + status = _winapi_AcceptSecurityContext( + ctypes.byref(Context.Credential), + Context.phContext if Context.phContext else None, + Input and ctypes.byref(Input), + ASC_REQ_FLAGS.from_GSS(Context.flags) + | ASC_REQ_FLAGS.ASC_REQ_ALLOCATE_MEMORY, + SECURITY_NETWORK_DREP, + ctypes.byref(phNewContext), + ctypes.byref(Output), + ctypes.byref(pfContextAttr), + ctypes.byref(Context.ptsExpiry), + ) + + # Find the output token, if any + output_token = None + if status in [ + SEC_CODES.SEC_E_OK, + SEC_CODES.SEC_I_CONTINUE_NEEDED, + SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE, + ]: + if Context.phContext is None: + Context.phContext = phNewContext + + # Extract output token + output_token = SecBufferDesc.ParseBuffer( + OutputBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB + ) + + # If we succeeded, query the session key + if status in [SEC_CODES.SEC_E_OK, SEC_CODES.SEC_I_COMPLETE_AND_CONTINUE]: + Context.QuerySessionKey() + Context.QueryNegotiatedFlags() + Context.QueryPkgContextSizes() + Context.state = self.STATE.COMPLETED + + # Free things we did not create (won't be freed by GC) + for OutputBuffer in OutputBuffers: + if OutputBuffer.pvBuffer is not None: + _winapi_FreeContextBuffer(OutputBuffer.pvBuffer) + + return Context, output_token, SEC_CODES.to_GSS(status) + + def LegsAmount(self, Context: CONTEXT): + if self.Package == "NTLM": + return 3 + else: + return 2 + + def MaximumSignatureLength(self, Context: CONTEXT): + return Context.cbMaxSignature + + def GSS_GetMICEx(self, Context, msgs, qop_req=0): + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG(SECBUFFER_DATA | SECBUFFER_READONLY), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(Context.cbMaxSignature), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(Context.cbMaxSignature), + ctypes.c_void_p, + ), + ) + ] + ) + # Call MakeSignature + status = _winapi_MakeSignature( + Context.phContext, + ctypes.wintypes.ULONG(qop_req), + ctypes.byref(Message), + 0, + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"MakeSignature failed with: {hex(status)}") + # Extract output token + sig = SecBufferDesc.ParseBuffer( + MessageBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB_SIGNATURE + ) + return sig + + def GSS_VerifyMICEx(self, Context, msgs, signature): + fQOP = ctypes.wintypes.ULONG(0) + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG(SECBUFFER_DATA | SECBUFFER_READONLY), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(len(signature)), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(bytes(signature)), ctypes.c_void_p + ), + ) + ] + ) + # Call VerifySignature + status = _winapi_VerifySignature( + Context.phContext, + ctypes.byref(Message), + 0, + ctypes.byref(fQOP), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"VerifySignature failed with: {hex(status)}") + + def GSS_WrapEx(self, Context, msgs, qop_req=0): + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG( + SECBUFFER_DATA + | (SECBUFFER_READONLY if not x.conf_req_flag else 0) + ), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(Context.cbSecurityTrailer), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(Context.cbSecurityTrailer), + ctypes.c_void_p, + ), + ) + ] + ) + # Call EncryptMessage + status = _winapi_EncryptMessage( + Context.phContext, + ctypes.wintypes.ULONG(qop_req), + ctypes.byref(Message), + 0, + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"EncryptMessage failed with: {hex(status)}") + # Update messages + for i in range(len(msgs)): + msgs[i].data = MessageBuffers[i].GetData() + # Extract signature + sig = SecBufferDesc.ParseBuffer( + MessageBuffers, SECBUFFER_TOKEN, GSSAPI_BLOB_SIGNATURE + ) + return ( + msgs, + sig, + ) + + def GSS_UnwrapEx(self, Context, msgs, signature): + fQOP = ctypes.wintypes.ULONG(0) + MessageBuffers, Message = SecBufferDesc.Create( + [ + SecBuffer( + ctypes.wintypes.ULONG(len(x.data)), + ctypes.wintypes.ULONG( + SECBUFFER_DATA + | (SECBUFFER_READONLY if not x.conf_req_flag else 0) + ), + ctypes.cast(ctypes.create_string_buffer(x.data), ctypes.c_void_p), + ) + for x in msgs + if x.sign + ] + + [ + SecBuffer( + ctypes.wintypes.ULONG(len(signature)), + ctypes.wintypes.ULONG(SECBUFFER_TOKEN), + ctypes.cast( + ctypes.create_string_buffer(bytes(signature)), ctypes.c_void_p + ), + ) + ] + ) + # Call DecryptMessage + status = _winapi_DecryptMessage( + Context.phContext, + ctypes.byref(Message), + 0, + ctypes.byref(fQOP), + ) + if status != SEC_CODES.SEC_E_OK: + raise ValueError(f"DecryptMessage failed with: {hex(status)}") + # Update messages + for i in range(len(msgs)): + msgs[i].data = MessageBuffers[i].GetData() + return msgs diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 23899274e4a..e13ba1d3f44 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -28,6 +28,7 @@ ASN1_Object, _ASN1_ERROR, ) +from scapy.libs.codec import GenericCodec_metaclass, GenericCodecObject from typing import ( Any, @@ -267,42 +268,37 @@ def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # [ BER classes ] # -class BERcodec_metaclass(type): - def __new__(cls, - name, # type: str - bases, # type: Tuple[type, ...] - dct # type: Dict[str, Any] - ): - # type: (...) -> Type[BERcodec_Object[Any]] - c = cast('Type[BERcodec_Object[Any]]', - super(BERcodec_metaclass, cls).__new__(cls, name, bases, dct)) - try: - c.tag.register(c.codec, c) - except Exception: - warning("Error registering %r for %r" % (c.tag, c.codec)) - return c +class BERcodec_metaclass(GenericCodec_metaclass): + """Metaclass for BER codec objects. + + Inherits the tag registration logic from ``GenericCodec_metaclass`` and + adds a BER-specific warning when registration fails. + """ + + @classmethod + def _handle_registration_error(cls, c, exc): + # type: (Type[Any], Exception) -> None + warning("Error registering %r for %r" % (c.tag, c.codec)) _K = TypeVar('_K') -class BERcodec_Object(Generic[_K], metaclass=BERcodec_metaclass): +class BERcodec_Object(GenericCodecObject[_K], metaclass=BERcodec_metaclass): codec = ASN1_Codecs.BER tag = ASN1_Class_UNIVERSAL.ANY + # Attributes consumed by GenericCodecObject.check_string and .dec + _decoding_error_class = BER_Decoding_Error + _generic_error_classes = (BER_Decoding_Error, ASN1_Error) # type: ignore + _decoding_error_object_class = ASN1_DECODING_ERROR + @classmethod def asn1_object(cls, val): # type: (_K) -> ASN1_Object[_K] return cls.tag.asn1_object(val) - @classmethod - def check_string(cls, s): - # type: (bytes) -> None - if not s: - raise BER_Decoding_Error( - "%s: Got empty object while expecting tag %r" % - (cls.__name__, cls.tag), remaining=s - ) + # check_string is inherited from GenericCodecObject (uses _decoding_error_class) @classmethod def check_type(cls, s): @@ -369,6 +365,12 @@ def dec(cls, safe=False, # type: bool ): # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + # BER overrides dec from GenericCodecObject to add special recovery for + # BER_BadTag_Decoding_Error: instead of wrapping the error in a + # DECODING_ERROR object, it recursively tries to decode from the + # remaining bytes and wraps the result in ASN1_BADTAG. + # Other BER/ASN1 errors are handled inline (same semantics as the + # generic dec, but with BER-specific exception types). if not safe: return cls.do_dec(s, context, safe) try: @@ -383,13 +385,7 @@ def dec(cls, except ASN1_Error as e: return ASN1_DECODING_ERROR(s, exc=e), b"" - @classmethod - def safedec(cls, - s, # type: bytes - context=None, # type: Optional[Type[ASN1_Class]] - ): - # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] - return cls.dec(s, context, safe=True) + # safedec is inherited from GenericCodecObject @classmethod def enc(cls, s, size_len=0): diff --git a/scapy/asn1/mib.py b/scapy/asn1/mib.py index 7bb30811e00..b164fcc9c44 100644 --- a/scapy/asn1/mib.py +++ b/scapy/asn1/mib.py @@ -807,6 +807,7 @@ def load_mib(filenames): # of some algorithms from pkcs1_oids and x962Signature_oids. hash_by_oid = { + "1.2.840.113549.1.1.1": "sha1", "1.2.840.113549.1.1.2": "md2", "1.2.840.113549.1.1.3": "md4", "1.2.840.113549.1.1.4": "md5", diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9a5284bddfc..6f7418ccde3 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -8,8 +8,6 @@ Classes that implement ASN.1 data structures. """ -import copy - from functools import reduce from scapy.asn1.asn1 import ( @@ -30,7 +28,11 @@ BER_tagging_dec, BER_tagging_enc, ) -from scapy.base_classes import BasePacket +from scapy.libs.codec import ( + GenericCodecField, + GenericCodecField_element, + GenericCodecOptionalField, +) from scapy.volatile import ( GeneralizedTime, RandChoice, @@ -48,7 +50,6 @@ AnyStr, Callable, Dict, - Generic, List, Optional, Tuple, @@ -67,7 +68,7 @@ class ASN1F_badsequence(Exception): pass -class ASN1F_element(object): +class ASN1F_element(GenericCodecField_element): pass @@ -79,11 +80,10 @@ class ASN1F_element(object): _A = TypeVar('_A') # ASN.1 object -class ASN1F_field(ASN1F_element, Generic[_I, _A]): - holds_packets = 0 - islist = 0 +class ASN1F_field(GenericCodecField[_I, _A], ASN1F_element): ASN1_tag = ASN1_Class_UNIVERSAL.ANY context = ASN1_Class_UNIVERSAL # type: Type[ASN1_Class] + _badsequence_error_class = ASN1F_badsequence def __init__(self, name, # type: str @@ -115,18 +115,6 @@ def __init__(self, self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] - def register_owner(self, cls): - # type: (Type[ASN1_Packet]) -> None - self.owners.append(cls) - - def i2repr(self, pkt, x): - # type: (ASN1_Packet, _I) -> str - return repr(x) - - def i2h(self, pkt, x): - # type: (ASN1_Packet, _I) -> Any - return x - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[_A, bytes] """ @@ -176,74 +164,10 @@ def i2m(self, pkt, x): implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) - def any2i(self, pkt, x): - # type: (ASN1_Packet, Any) -> _I - return cast(_I, x) - - def extract_packet(self, - cls, # type: Type[ASN1_Packet] - s, # type: bytes - _underlayer=None # type: Optional[ASN1_Packet] - ): - # type: (...) -> Tuple[ASN1_Packet, bytes] - try: - c = cls(s, _underlayer=_underlayer) - except ASN1F_badsequence: - c = packet.Raw(s, _underlayer=_underlayer) # type: ignore - cpad = c.getlayer(packet.Raw) - s = b"" - if cpad is not None: - s = cpad.load - if cpad.underlayer: - del cpad.underlayer.payload - return c, s - - def build(self, pkt): - # type: (ASN1_Packet) -> bytes - return self.i2m(pkt, getattr(pkt, self.name)) - - def dissect(self, pkt, s): - # type: (ASN1_Packet, bytes) -> bytes - v, s = self.m2i(pkt, s) - self.set_val(pkt, v) - return s - - def do_copy(self, x): - # type: (Any) -> Any - if isinstance(x, list): - x = x[:] - for i in range(len(x)): - if isinstance(x[i], BasePacket): - x[i] = x[i].copy() - return x - if hasattr(x, "copy"): - return x.copy() - return x - - def set_val(self, pkt, val): - # type: (ASN1_Packet, Any) -> None - setattr(pkt, self.name, val) - - def is_empty(self, pkt): - # type: (ASN1_Packet) -> bool - return getattr(pkt, self.name) is None - - def get_fields_list(self): - # type: () -> List[ASN1F_field[Any, Any]] - return [self] - - def __str__(self): - # type: () -> str - return repr(self) - def randval(self): # type: () -> RandField[_I] return cast(RandField[_I], RandInt()) - def copy(self): - # type: () -> ASN1F_field[_I, _A] - return copy.copy(self) - ############################ # Simple ASN1 Fields # @@ -640,24 +564,24 @@ class ASN1F_TIME_TICKS(ASN1F_INTEGER): # Complex ASN1 Fields # ############################# -class ASN1F_optional(ASN1F_element): +class ASN1F_optional(GenericCodecOptionalField, ASN1F_element): """ ASN.1 field that is optional. """ + _optional_error_classes = ( # type: ignore + ASN1_Error, ASN1F_badsequence, BER_Decoding_Error + ) + def __init__(self, field): # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False self._field = field - def __getattr__(self, attr): - # type: (str) -> Optional[Any] - return getattr(self._field, attr) - def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] try: return self._field.m2i(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except self._optional_error_classes: # ASN1_Error may be raised by ASN1F_CHOICE return None, s @@ -665,24 +589,10 @@ def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes try: return self._field.dissect(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except self._optional_error_classes: self._field.set_val(pkt, None) return s - def build(self, pkt): - # type: (ASN1_Packet) -> bytes - if self._field.is_empty(pkt): - return b"" - return self._field.build(pkt) - - def any2i(self, pkt, x): - # type: (ASN1_Packet, Any) -> Any - return self._field.any2i(pkt, x) - - def i2repr(self, pkt, x): - # type: (ASN1_Packet, Any) -> str - return self._field.i2repr(pkt, x) - class ASN1F_omit(ASN1F_field[None, None]): """ diff --git a/scapy/automaton.py b/scapy/automaton.py index 2aae9168725..5f3ffcc12cd 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -681,6 +681,9 @@ def recv(self, n=MTU, **kwargs): # type: ignore r = self.spb.recv(n) if self.proto is not None and r is not None: r = self.proto(r, **kwargs) + if self.atmt.atmt_session is not None: + # Apply session if provided + r = self.atmt.atmt_session.process(r) return r def close(self): @@ -962,7 +965,8 @@ def parse_args(self, debug=0, store=0, session=None, **kargs): self.debug_level = debug if debug: conf.logLevel = logging.DEBUG - self.atmt_session = session + if session: + self.atmt_session = session self.socket_kargs = kargs self.store_packets = store diff --git a/scapy/autorun.py b/scapy/autorun.py index 1e5d4b10d26..a0ee9e7469e 100644 --- a/scapy/autorun.py +++ b/scapy/autorun.py @@ -84,10 +84,12 @@ def autorun_commands(_cmds, my_globals=None, verb=None): if interp.runsource(cmd): continue if sys.last_value: # An error occurred - traceback.print_exception(sys.last_type, - sys.last_value, - sys.last_traceback.tb_next, - file=sys.stdout) + traceback.print_exception( + sys.last_type, + sys.last_value, + sys.last_traceback.tb_next, + file=sys.stdout, + ) sys.last_value = None return False cmd = "" diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index de462e74528..dcec5d8ed5d 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -44,6 +44,26 @@ CBORcodec_SIMPLE_AND_FLOAT, ) +from scapy.cbor.cborfields import ( + CBORF_element, + CBORF_field, + CBORF_UNSIGNED_INTEGER, + CBORF_NEGATIVE_INTEGER, + CBORF_INTEGER, + CBORF_BYTE_STRING, + CBORF_TEXT_STRING, + CBORF_BOOLEAN, + CBORF_NULL, + CBORF_UNDEFINED, + CBORF_FLOAT, + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_MAP, + CBORF_SEMANTIC_TAG, + CBORF_optional, + CBORF_PACKET, +) + __all__ = [ # Exceptions "CBOR_Error", @@ -81,4 +101,25 @@ "CBORcodec_MAP", "CBORcodec_SEMANTIC_TAG", "CBORcodec_SIMPLE_AND_FLOAT", + # Field base classes + "CBORF_element", + "CBORF_field", + # Scalar fields + "CBORF_UNSIGNED_INTEGER", + "CBORF_NEGATIVE_INTEGER", + "CBORF_INTEGER", + "CBORF_BYTE_STRING", + "CBORF_TEXT_STRING", + "CBORF_BOOLEAN", + "CBORF_NULL", + "CBORF_UNDEFINED", + "CBORF_FLOAT", + # Structured fields + "CBORF_ARRAY", + "CBORF_ARRAY_OF", + "CBORF_MAP", + "CBORF_SEMANTIC_TAG", + # Complex fields + "CBORF_optional", + "CBORF_PACKET", ] diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index b49b9d38b30..9b4e72a2c26 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -33,6 +33,7 @@ ) from scapy.compat import chb, orb from scapy.error import log_runtime +from scapy.libs.codec import GenericCodec_metaclass, GenericCodecObject ################## @@ -142,43 +143,38 @@ def CBOR_decode_head(s): # [ CBOR codec classes ] # -class CBORcodec_metaclass(type): - def __new__(cls, - name, # type: str - bases, # type: Tuple[type, ...] - dct # type: Dict[str, Any] - ): - # type: (...) -> Type[CBORcodec_Object[Any]] - c = cast('Type[CBORcodec_Object[Any]]', - super(CBORcodec_metaclass, cls).__new__(cls, name, bases, dct)) - try: - c.tag.register(c.codec, c) - except Exception: - log_runtime.error("Failed to register codec for tag") - return c +class CBORcodec_metaclass(GenericCodec_metaclass): + """Metaclass for CBOR codec objects. + + Inherits the tag registration logic from ``GenericCodec_metaclass`` and + adds a CBOR-specific log message when registration fails. + """ + + @classmethod + def _handle_registration_error(cls, c, exc): + # type: (Type[Any], Exception) -> None + log_runtime.error("Failed to register codec for tag") _K = TypeVar('_K') -class CBORcodec_Object(Generic[_K], metaclass=CBORcodec_metaclass): +class CBORcodec_Object(GenericCodecObject[_K], metaclass=CBORcodec_metaclass): """Base CBOR codec class""" codec = CBOR_Codecs.CBOR tag = CBOR_MajorTypes.UNSIGNED_INTEGER + # Attributes consumed by GenericCodecObject.check_string and .dec + _decoding_error_class = CBOR_Codec_Decoding_Error + _generic_error_classes = (CBOR_Codec_Decoding_Error, CBOR_Error) # type: ignore + _decoding_error_object_class = CBOR_DECODING_ERROR + @classmethod def cbor_object(cls, val): # type: (_K) -> CBOR_Object[_K] return cls.tag.cbor_object(val) - @classmethod - def check_string(cls, s): - # type: (bytes) -> None - if not s: - raise CBOR_Codec_Decoding_Error( - "%s: Got empty object while expecting tag %r" % - (cls.__name__, cls.tag), remaining=s - ) + # check_string is inherited from GenericCodecObject (uses _decoding_error_class) @classmethod def do_dec(cls, @@ -190,29 +186,10 @@ def do_dec(cls, """Decode CBOR data using automatic dispatch based on major type.""" return _decode_cbor_item(s, safe=safe) - @classmethod - def dec(cls, - s, # type: bytes - context=None, # type: Optional[Any] - safe=False, # type: bool - ): - # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] - if not safe: - return cls.do_dec(s, context, safe) - try: - return cls.do_dec(s, context, safe) - except CBOR_Codec_Decoding_Error as e: - return CBOR_DECODING_ERROR(s, exc=e), b"" - except CBOR_Error as e: - return CBOR_DECODING_ERROR(s, exc=e), b"" + # dec is inherited from GenericCodecObject (uses _generic_error_classes and + # _decoding_error_object_class) - @classmethod - def safedec(cls, - s, # type: bytes - context=None, # type: Optional[Any] - ): - # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] - return cls.dec(s, context, safe=True) + # safedec is inherited from GenericCodecObject @classmethod def enc(cls, s): diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py new file mode 100644 index 00000000000..a0963763e91 --- /dev/null +++ b/scapy/cbor/cborfields.py @@ -0,0 +1,796 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Classes that implement CBOR (Concise Binary Object Representation) data +structures as packet fields. Modelled after scapy/asn1fields.py. +""" + +from functools import reduce + +from scapy.cbor.cbor import ( + CBOR_Decoding_Error, + CBOR_Error, + CBOR_MajorTypes, + CBOR_Object, + CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER, + CBOR_BYTE_STRING, + CBOR_TEXT_STRING, + CBOR_SEMANTIC_TAG, + CBOR_FALSE, + CBOR_TRUE, + CBOR_NULL, + CBOR_UNDEFINED, + CBOR_FLOAT, +) +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + CBOR_decode_head, + CBOR_encode_head, + CBORcodec_Object, + CBORcodec_UNSIGNED_INTEGER, + CBORcodec_NEGATIVE_INTEGER, + CBORcodec_BYTE_STRING, + CBORcodec_TEXT_STRING, + CBORcodec_SIMPLE_AND_FLOAT, +) +from scapy.libs.codec import ( + GenericCodecField, + GenericCodecField_element, + GenericCodecOptionalField, +) +from scapy.volatile import ( + RandChoice, + RandFloat, + RandNum, + RandString, + RandField, +) + +from scapy import packet + +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.cborpacket import CBOR_Packet # noqa: F401 + + +class CBORF_badsequence(Exception): + pass + + +class CBORF_element(GenericCodecField_element): + pass + + +########################## +# Basic CBOR Field # +########################## + +_I = TypeVar('_I') # Internal storage +_A = TypeVar('_A') # CBOR object + + +class CBORF_field(GenericCodecField[_I, _A], CBORF_element): + CBOR_tag = None # type: Optional[Any] + _badsequence_error_class = CBORF_badsequence + + def __init__(self, + name, # type: str + default, # type: Optional[_A] + ): + # type: (...) -> None + self.name = name + if default is None: + self.default = default # type: Optional[_A] + else: + self.default = self._wrap(default) + self.owners = [] # type: List[Type[CBOR_Packet]] + + def _wrap(self, val): + # type: (Any) -> _A + """Return a CBOR object wrapping *val*. + + The base implementation is a pass-through cast; subclasses override + this to convert a raw Python value to the appropriate CBOR object + type (e.g. :class:`~scapy.cbor.cbor.CBOR_UNSIGNED_INTEGER`). + """ + return cast(_A, val) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[_A, bytes] + raise NotImplementedError("Subclasses must implement m2i") + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Union[bytes, _I, _A]) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_Object): + return x.enc() + return self._encode(x) + + def _encode(self, x): + # type: (Any) -> bytes + """Encode a raw Python value to CBOR bytes.""" + raise NotImplementedError("Subclasses must implement _encode") + + def randval(self): + # type: () -> RandField[_I] + return cast(RandField[_I], RandNum(0, 2 ** 32)) + + +############################# +# Simple CBOR Fields # +############################# + +class CBORF_UNSIGNED_INTEGER(CBORF_field[int, CBOR_UNSIGNED_INTEGER]): + """CBOR unsigned integer field (major type 0).""" + CBOR_tag = CBOR_MajorTypes.UNSIGNED_INTEGER + + def _wrap(self, val): + # type: (Any) -> CBOR_UNSIGNED_INTEGER + if isinstance(val, CBOR_UNSIGNED_INTEGER): + return val + return CBOR_UNSIGNED_INTEGER(int(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNSIGNED_INTEGER, bytes] + return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_UNSIGNED_INTEGER.enc( + x if isinstance(x, CBOR_Object) else CBOR_UNSIGNED_INTEGER(int(x)) + ) + + def randval(self): + # type: () -> RandNum + return RandNum(0, 2 ** 64 - 1) + + +class CBORF_NEGATIVE_INTEGER(CBORF_field[int, CBOR_NEGATIVE_INTEGER]): + """CBOR negative integer field (major type 1).""" + CBOR_tag = CBOR_MajorTypes.NEGATIVE_INTEGER + + def _wrap(self, val): + # type: (Any) -> CBOR_NEGATIVE_INTEGER + if isinstance(val, CBOR_NEGATIVE_INTEGER): + return val + return CBOR_NEGATIVE_INTEGER(int(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NEGATIVE_INTEGER, bytes] + return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_NEGATIVE_INTEGER.enc( + x if isinstance(x, CBOR_Object) else CBOR_NEGATIVE_INTEGER(int(x)) + ) + + def randval(self): + # type: () -> RandNum + return RandNum(-2 ** 64, -1) + + +class CBORF_INTEGER(CBORF_field[int, + Union[CBOR_UNSIGNED_INTEGER, + CBOR_NEGATIVE_INTEGER]]): + """CBOR integer field handling both positive and negative values.""" + + def _wrap(self, val): + # type: (Any) -> Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER] + if isinstance(val, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): + return val + i = int(val) + if i >= 0: + return CBOR_UNSIGNED_INTEGER(i) + return CBOR_NEGATIVE_INTEGER(i) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER], bytes] # noqa: E501 + if not s: + raise CBOR_Decoding_Error("Empty CBOR data") + major_type = (s[0] >> 5) & 0x7 + if major_type == 0: + return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore + elif major_type == 1: + return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore + raise CBOR_Decoding_Error( + "Expected integer (major type 0 or 1), got %d" % major_type) + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_Object): + return x.enc() + i = int(x) + if i >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(CBOR_UNSIGNED_INTEGER(i)) + return CBORcodec_NEGATIVE_INTEGER.enc(CBOR_NEGATIVE_INTEGER(i)) + + def randval(self): + # type: () -> RandNum + return RandNum(-2 ** 64, 2 ** 64 - 1) + + +class CBORF_BYTE_STRING(CBORF_field[bytes, CBOR_BYTE_STRING]): + """CBOR byte string field (major type 2).""" + CBOR_tag = CBOR_MajorTypes.BYTE_STRING + + def _wrap(self, val): + # type: (Any) -> CBOR_BYTE_STRING + if isinstance(val, CBOR_BYTE_STRING): + return val + return CBOR_BYTE_STRING(bytes(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_BYTE_STRING, bytes] + return CBORcodec_BYTE_STRING.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_BYTE_STRING.enc( + x if isinstance(x, CBOR_Object) else CBOR_BYTE_STRING(bytes(x)) + ) + + def randval(self): + # type: () -> RandString + return RandString(RandNum(0, 1000)) + + +class CBORF_TEXT_STRING(CBORF_field[str, CBOR_TEXT_STRING]): + """CBOR text string field (major type 3).""" + CBOR_tag = CBOR_MajorTypes.TEXT_STRING + + def _wrap(self, val): + # type: (Any) -> CBOR_TEXT_STRING + if isinstance(val, CBOR_TEXT_STRING): + return val + return CBOR_TEXT_STRING(str(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_TEXT_STRING, bytes] + return CBORcodec_TEXT_STRING.dec(s) # type: ignore + + def _encode(self, x): + # type: (Any) -> bytes + return CBORcodec_TEXT_STRING.enc( + x if isinstance(x, CBOR_Object) else CBOR_TEXT_STRING(str(x)) + ) + + def randval(self): + # type: () -> RandString + return RandString(RandNum(0, 1000)) + + +class CBORF_BOOLEAN(CBORF_field[bool, Union[CBOR_FALSE, CBOR_TRUE]]): + """CBOR boolean field (major type 7, simple values 20/21).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def _wrap(self, val): + # type: (Any) -> Union[CBOR_FALSE, CBOR_TRUE] + if isinstance(val, (CBOR_FALSE, CBOR_TRUE)): + return val + return CBOR_TRUE() if val else CBOR_FALSE() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_FALSE, CBOR_TRUE], bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, (CBOR_FALSE, CBOR_TRUE)): + raise CBOR_Decoding_Error( + "Expected boolean (CBOR_FALSE or CBOR_TRUE), got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, (CBOR_FALSE, CBOR_TRUE)): + return x.enc() + return CBORcodec_SIMPLE_AND_FLOAT.enc( + CBOR_TRUE() if x else CBOR_FALSE() + ) + + def randval(self): + # type: () -> RandChoice + return RandChoice(True, False) + + +class CBORF_NULL(CBORF_field[None, CBOR_NULL]): + """CBOR null field (major type 7, simple value 22).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self, + name, # type: str + default=None, # type: None + ): + # type: (...) -> None + super(CBORF_NULL, self).__init__(name, None) + + def _wrap(self, val): + # type: (Any) -> CBOR_NULL + return CBOR_NULL() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NULL, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_NULL): + raise CBOR_Decoding_Error( + "Expected null, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + return CBOR_NULL().enc() + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return False + + +class CBORF_UNDEFINED(CBORF_field[None, CBOR_UNDEFINED]): + """CBOR undefined field (major type 7, simple value 23).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def __init__(self, + name, # type: str + default=None, # type: None + ): + # type: (...) -> None + super(CBORF_UNDEFINED, self).__init__(name, None) + + def _wrap(self, val): + # type: (Any) -> CBOR_UNDEFINED + return CBOR_UNDEFINED() + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNDEFINED, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_UNDEFINED): + raise CBOR_Decoding_Error( + "Expected undefined, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + return CBOR_UNDEFINED().enc() + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return False + + +class CBORF_FLOAT(CBORF_field[float, CBOR_FLOAT]): + """CBOR float field (major type 7, double precision).""" + CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + + def _wrap(self, val): + # type: (Any) -> CBOR_FLOAT + if isinstance(val, CBOR_FLOAT): + return val + return CBOR_FLOAT(float(val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_FLOAT, bytes] + obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) + if not isinstance(obj, CBOR_FLOAT): + raise CBOR_Decoding_Error( + "Expected float, got %r" % obj) + return obj, remain # type: ignore + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, CBOR_FLOAT): + return x.enc() + return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_FLOAT(float(x))) + + def randval(self): + # type: () -> RandFloat + return RandFloat(0, 2 ** 32) + + +############################## +# Structured CBOR Fields # +############################## + +class CBORF_ARRAY(CBORF_field[List[Any], List[Any]]): + """ + CBOR array with a fixed sequence of named, typed fields (major type 4). + Analogous to ASN1F_SEQUENCE: each positional element corresponds to a + specific CBORF_field. The CBOR array count must match the number of + declared fields. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + CBOR_tag = CBOR_MajorTypes.ARRAY + holds_packets = 1 + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + # The array itself is a structural field without its own named slot on + # the packet; a placeholder name is used so the base class __init__ + # stays happy. Individual element fields are the ones that carry names. + name = "_cbor_array" + default = [field.default for field in seq] + super(CBORF_ARRAY, self).__init__(name, None) + self.default = default + self.seq = seq + self.islist = len(seq) > 1 + + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.seq) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return all(f.is_empty(pkt) for f in self.seq) + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return reduce(lambda x, y: x + y.get_fields_list(), + self.seq, []) + + def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + """ + Decode a CBOR array. Each element is decoded by its corresponding + field in ``self.seq``. The decoded values are set directly on the + packet by each field's ``dissect`` call, so this method returns an + empty list (which is discarded by ``dissect``). + """ + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 4: + raise CBOR_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type) + if count != len(self.seq): + raise CBOR_Decoding_Error( + "Array length mismatch: expected %d, got %d" % + (len(self.seq), count)) + for obj in self.seq: + try: + s = obj.dissect(pkt, s) + except CBORF_badsequence: + break + return [], s + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + _, x = self.m2i(pkt, s) + return x + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + items = b"".join(obj.build(pkt) for obj in self.seq) + return CBOR_encode_head(4, len(self.seq)) + items + + +_ARRAY_T = Union[ + 'CBOR_Packet', + Type[CBORF_field[Any, Any]], + 'CBORF_PACKET', + CBORF_field[Any, Any], +] + + +class CBORF_ARRAY_OF(CBORF_field[List[_ARRAY_T], List[CBOR_Object[Any]]]): + """ + CBOR array of homogeneous elements (major type 4). + Analogous to ASN1F_SEQUENCE_OF: variable-length array where every + element shares the same type, specified by ``cls``. + + ``cls`` may be a :class:`CBORF_field` class/instance (leaf type) or a + :class:`CBOR_Packet` subclass (structured type). + """ + CBOR_tag = CBOR_MajorTypes.ARRAY + islist = 1 + + def __init__(self, + name, # type: str + default, # type: Any + cls, # type: _ARRAY_T + ): + # type: (...) -> None + if isinstance(cls, type) and issubclass(cls, CBORF_field) or \ + isinstance(cls, CBORF_field): + if isinstance(cls, type): + self.fld = cls("_item", None) # type: ignore + else: + self.fld = cls + self._extract_item = lambda s, pkt: self.fld.m2i(pkt, s) + self.holds_packets = 0 + elif hasattr(cls, "CBOR_root") or callable(cls): + self.cls = cast("Type[CBOR_Packet]", cls) + self._extract_item = lambda s, pkt: self.extract_packet( + self.cls, s, _underlayer=pkt) + self.holds_packets = 1 + else: + raise ValueError("cls must be a CBORF_field or CBOR_Packet") + super(CBORF_ARRAY_OF, self).__init__(name, None) + self.default = default + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return CBORF_field.is_empty(self, pkt) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 4: + raise CBOR_Decoding_Error( + "Expected major type 4 (array), got %d" % major_type) + lst = [] + for _ in range(count): + c, s = self._extract_item(s, pkt) # type: ignore + if c is not None: + lst.append(c) + return lst, s + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + val = getattr(pkt, self.name) + if val is None: + val = [] + items = b"".join(bytes(item) for item in val) + return CBOR_encode_head(4, len(val)) + items + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if self.holds_packets: + return repr(x) + elif x is None: + return "[]" + else: + return "[%s]" % ", ".join( + self.fld.i2repr(pkt, item) for item in x # type: ignore + ) + + def __repr__(self): + # type: () -> str + return "<%s %s>" % (self.__class__.__name__, self.name) + + +class CBORF_MAP(CBORF_field[Dict[str, Any], Dict[str, Any]]): + """ + CBOR map with a fixed set of named, typed fields (major type 5). + + Each field in ``seq`` represents one key-value pair. The key is the + field's ``name`` encoded as a CBOR text string. The value is encoded + and decoded by the corresponding :class:`CBORF_field`. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + CBOR_tag = CBOR_MajorTypes.MAP + holds_packets = 1 + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + # The map itself is a structural field without its own named slot on + # the packet; a placeholder name is used so the base class __init__ + # stays happy. Individual value fields are the ones that carry names + # (which also serve as the CBOR text-string keys in the wire encoding). + name = "_cbor_map" + default = {field.name: field.default for field in seq} + super(CBORF_MAP, self).__init__(name, None) + self.default = default + self.seq = seq + self.islist = 1 + + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.seq) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return all(f.is_empty(pkt) for f in self.seq) + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return reduce(lambda x, y: x + y.get_fields_list(), + self.seq, []) + + def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + """ + Decode a CBOR map. Keys are decoded as CBOR items and matched to + fields by name. Values are decoded by the matching field. Unknown + keys are silently skipped. + """ + try: + major_type, count, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 5: + raise CBOR_Decoding_Error( + "Expected major type 5 (map), got %d" % major_type) + # Build a lookup from field name to field object. + field_map = {f.name: f for f in self.seq} + for _ in range(count): + # Decode the key (any CBOR type; convert to str for lookup). + key_obj, s = CBORcodec_Object.decode_cbor_item(s) + if isinstance(key_obj, CBOR_Object): + key = str(key_obj.val) + else: + key = str(key_obj) + fld = field_map.get(key) + if fld is not None: + s = fld.dissect(pkt, s) + else: + # Skip unknown value. + _unknown, s = CBORcodec_Object.decode_cbor_item(s) + return [], s + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + _, x = self.m2i(pkt, s) + return x + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + result = CBOR_encode_head(5, len(self.seq)) + for fld in self.seq: + # Encode key as a CBOR text string. + result += CBORcodec_TEXT_STRING.enc(CBOR_TEXT_STRING(fld.name)) + result += fld.build(pkt) + return result + + +class CBORF_SEMANTIC_TAG(CBORF_field[Tuple[int, Any], + CBOR_SEMANTIC_TAG]): + """ + CBOR semantic tag field (major type 6). + + Wraps an ``inner_field`` with the given numeric ``tag_num``. The inner + field handles encoding and decoding of the tagged value. The outer field + (named ``name``) stores the :class:`~scapy.cbor.cbor.CBOR_SEMANTIC_TAG` + wrapper (tag number + ``None`` placeholder), while the inner field stores + its value under its own name on the packet. + + Example:: + + class TimestampPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + "tag_info", None, 1, CBORF_INTEGER("ts", 0) + ) + """ + CBOR_tag = CBOR_MajorTypes.TAG + + def __init__(self, + name, # type: str + default, # type: Any + tag_num, # type: int + inner_field, # type: CBORF_field[Any, Any] + ): + # type: (...) -> None + self.tag_num = tag_num + self.inner_field = inner_field + super(CBORF_SEMANTIC_TAG, self).__init__(name, default) + + def _wrap(self, val): + # type: (Any) -> CBOR_SEMANTIC_TAG + if isinstance(val, CBOR_SEMANTIC_TAG): + return val + return CBOR_SEMANTIC_TAG((self.tag_num, val)) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_SEMANTIC_TAG, bytes] + try: + major_type, tag_num, s = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != 6: + raise CBOR_Decoding_Error( + "Expected major type 6 (semantic tag), got %d" % major_type) + return CBOR_SEMANTIC_TAG((tag_num, None)), s # type: ignore + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + tag_obj, s = self.m2i(pkt, s) + self.set_val(pkt, tag_obj) + # Dissect the tagged content using the inner field. + return self.inner_field.dissect(pkt, s) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + inner_bytes = self.inner_field.build(pkt) + return CBOR_encode_head(6, self.tag_num) + inner_bytes + + def get_fields_list(self): + # type: () -> List[CBORF_field[Any, Any]] + return [self] + self.inner_field.get_fields_list() + + +############################## +# Complex CBOR Fields # +############################## + +class CBORF_optional(GenericCodecOptionalField, CBORF_element): + """ + Wrapper making a :class:`CBORF_field` optional. + + During decoding, if the next CBOR item does not match the expected major + type, the field value is set to ``None`` and the stream is left unchanged. + """ + _optional_error_classes = ( # type: ignore + CBOR_Error, CBORF_badsequence, CBOR_Codec_Decoding_Error + ) + + def __init__(self, field): + # type: (CBORF_field[Any, Any]) -> None + self._field = field + + +class CBORF_PACKET(CBORF_field['CBOR_Packet', Optional['CBOR_Packet']]): + """ + CBOR field that encapsulates a nested :class:`CBOR_Packet`. + + The nested packet is encoded as-is (its ``CBOR_root.build()`` output) + and decoded by instantiating ``cls`` from the current byte stream. + """ + holds_packets = 1 + + def __init__(self, + name, # type: str + default, # type: Optional[CBOR_Packet] + cls, # type: Type[CBOR_Packet] + ): + # type: (...) -> None + self.cls = cls + super(CBORF_PACKET, self).__init__(name, None) + self.default = default + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + return self.extract_packet(self.cls, s, _underlayer=pkt) + + def i2m(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if x is None: + return b"" + if isinstance(x, bytes): + return x + return bytes(x) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> CBOR_Packet + if hasattr(x, "add_underlayer"): + x.add_underlayer(pkt) + return super(CBORF_PACKET, self).any2i(pkt, x) # type: ignore + + def randval(self): # type: ignore + # type: () -> CBOR_Packet + return packet.fuzz(self.cls()) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py new file mode 100644 index 00000000000..eb12bedaea9 --- /dev/null +++ b/scapy/cborpacket.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +CBOR Packet + +Packet holding data encoded in Concise Binary Object Representation (CBOR). +Modelled after scapy/asn1packet.py. +""" + +from scapy.base_classes import Packet_metaclass +from scapy.packet import Packet + +from typing import ( + Any, + Dict, + Tuple, + Type, + cast, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from scapy.cbor.cborfields import CBORF_field # noqa: F401 + + +class CBORPacket_metaclass(Packet_metaclass): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type[CBOR_Packet] + if dct.get("CBOR_root") is not None: + dct["fields_desc"] = dct["CBOR_root"].get_fields_list() + return cast( + 'Type[CBOR_Packet]', + super(CBORPacket_metaclass, cls).__new__(cls, name, bases, dct), + ) + + +class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): + CBOR_root = cast('CBORF_field[Any, Any]', None) + + def self_build(self): + # type: () -> bytes + """Build this CBOR packet to wire bytes using CBOR_root. + + Returns the raw packet cache when already built, otherwise delegates + to CBOR_root.build() which encodes all fields according to the CBOR + schema defined for this packet. + """ + if self.raw_packet_cache is not None: + return self.raw_packet_cache + return self.CBOR_root.build(self) + + def do_dissect(self, x): + # type: (bytes) -> bytes + """Dissect CBOR-encoded bytes into packet fields. + + Delegates to CBOR_root.dissect() which reads CBOR items from *x*, + populates each field on the packet, and returns any unconsumed bytes. + """ + return self.CBOR_root.dissect(self, x) diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 76c9cf110f1..62c88b208d7 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -32,6 +32,12 @@ from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.contrib.isotp import ISOTP +from scapy.compat import orb + +from typing import ( # noqa: F401 + Dict, + Type, +) """ GMLAN @@ -46,11 +52,38 @@ # "a negative response 'RequestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False} + conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None +def _gmlan_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['GMLAN']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`GMLAN` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`GMLAN` packet, preventing a duplicate + service byte when sub-packets are stacked (``GMLAN()/GMLAN_IDO()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['GMLAN'].get('single_layer_mode', False): + return False + if conf.contribs['GMLAN'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, GMLAN) + return True + + class GMLAN(ISOTP): @staticmethod def determine_len(x): @@ -130,6 +163,17 @@ def hashret(self): return struct.pack('B', self.requestServiceId & ~0x40) return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct GMLAN service class in single layer mode.""" + if conf.contribs['GMLAN'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + # ########################IDO################################### class GMLAN_IDO(Packet): @@ -139,11 +183,13 @@ class GMLAN_IDO(Packet): 0x04: 'wakeUpLinks'} name = 'InitiateDiagnosticOperation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] bind_layers(GMLAN, GMLAN_IDO, service=0x10) +GMLAN._service_cls[0x10] = GMLAN_IDO # ########################RFRD################################### @@ -166,18 +212,21 @@ class GMLAN_RFRD(Packet): 0x02: 'readFailureRecordParameters'} name = 'ReadFailureRecordData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x12, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), - ConditionalField(PacketField("dtc", b'', GMLAN_DTC), + ConditionalField(PacketField("dtc", None, GMLAN_DTC), lambda pkt: pkt.subfunction == 0x02) ] bind_layers(GMLAN, GMLAN_RFRD, service=0x12) +GMLAN._service_cls[0x12] = GMLAN_RFRD class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x52, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_RFRD.subfunctions) ] @@ -187,6 +236,7 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_RFRDPR, service=0x52) +GMLAN._service_cls[0x52] = GMLAN_RFRDPR class GMLAN_RFRDPR_RFRI(Packet): @@ -208,7 +258,7 @@ class GMLAN_RFRDPR_RFRI(Packet): class GMLAN_RFRDPR_RFRP(Packet): name = 'ReadFailureRecordDataPositiveResponse_readFailureRecordParameters' fields_desc = [ - PacketField("dtc", b'', GMLAN_DTC) + PacketField("dtc", None, GMLAN_DTC) ] @@ -304,16 +354,19 @@ class GMLAN_RDBI(Packet): name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x1a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, dataIdentifiers) ] -bind_layers(GMLAN, GMLAN_RDBI, service=0x1A) +bind_layers(GMLAN, GMLAN_RDBI, service=0x1a) +GMLAN._service_cls[0x1a] = GMLAN_RDBI class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x5a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), ] @@ -322,7 +375,8 @@ def answers(self, other): other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5A) +bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5a) +GMLAN._service_cls[0x5a] = GMLAN_RDBIPR # ########################RDBI################################### @@ -334,6 +388,7 @@ class GMLAN_RDBPI(Packet): }) name = 'ReadDataByParameterIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x22, GMLAN.services), _gmlan_slm), FieldListField("identifiers", [], XShortEnumField('parameterIdentifier', 0, dataIdentifiers)) @@ -341,11 +396,13 @@ class GMLAN_RDBPI(Packet): bind_layers(GMLAN, GMLAN_RDBPI, service=0x22) +GMLAN._service_cls[0x22] = GMLAN_RDBPI class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, GMLAN.services), _gmlan_slm), XShortEnumField('parameterIdentifier', 0, GMLAN_RDBPI.dataIdentifiers), ] @@ -355,6 +412,7 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_RDBPIPR, service=0x62) +GMLAN._service_cls[0x62] = GMLAN_RDBPIPR # ########################RDBPKTI################################### @@ -369,6 +427,7 @@ class GMLAN_RDBPKTI(Packet): } fields_desc = [ + ConditionalField(XByteEnumField('service', 0xaa, GMLAN.services), _gmlan_slm), XByteEnumField('subfunction', 0, subfunctions), ConditionalField(FieldListField('request_DPIDs', [], XByteField("", 0)), @@ -376,13 +435,15 @@ class GMLAN_RDBPKTI(Packet): ] -bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xAA) +bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xaa) +GMLAN._service_cls[0xaa] = GMLAN_RDBPKTI # ########################RMBA################################### class GMLAN_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x23, GMLAN.services), _gmlan_slm), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -398,11 +459,13 @@ class GMLAN_RMBA(Packet): bind_layers(GMLAN, GMLAN_RMBA, service=0x23) +GMLAN._service_cls[0x23] = GMLAN_RMBA class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x63, GMLAN.services), _gmlan_slm), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -422,6 +485,7 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_RMBAPR, service=0x63) +GMLAN._service_cls[0x63] = GMLAN_RMBAPR # ########################SA################################### @@ -443,6 +507,7 @@ class GMLAN_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ConditionalField(XShortField('securityKey', 0), lambda pkt: pkt.subfunction % 2 == 0) @@ -450,11 +515,13 @@ class GMLAN_SA(Packet): bind_layers(GMLAN, GMLAN_SA, service=0x27) +GMLAN._service_cls[0x27] = GMLAN_SA class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_SA.subfunctions), ConditionalField(XShortField('securitySeed', 0), lambda pkt: pkt.subfunction % 2 == 1), @@ -466,23 +533,27 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_SAPR, service=0x67) +GMLAN._service_cls[0x67] = GMLAN_SAPR # ########################DDM################################### class GMLAN_DDM(Packet): name = 'DynamicallyDefineMessage' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0), StrField('PIDData', b'\x00\x00') ] -bind_layers(GMLAN, GMLAN_DDM, service=0x2C) +bind_layers(GMLAN, GMLAN_DDM, service=0x2c) +GMLAN._service_cls[0x2c] = GMLAN_DDM class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0) ] @@ -491,13 +562,15 @@ def answers(self, other): and other.DPIDIdentifier == self.DPIDIdentifier -bind_layers(GMLAN, GMLAN_DDMPR, service=0x6C) +bind_layers(GMLAN, GMLAN_DDMPR, service=0x6c) +GMLAN._service_cls[0x6c] = GMLAN_DDMPR # ########################DPBA################################### class GMLAN_DPBA(Packet): name = 'DefinePIDByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), MultipleTypeField( [ @@ -513,12 +586,14 @@ class GMLAN_DPBA(Packet): ] -bind_layers(GMLAN, GMLAN_DPBA, service=0x2D) +bind_layers(GMLAN, GMLAN_DPBA, service=0x2d) +GMLAN._service_cls[0x2d] = GMLAN_DPBA class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), ] @@ -527,13 +602,15 @@ def answers(self, other): and other.parameterIdentifier == self.parameterIdentifier -bind_layers(GMLAN, GMLAN_DPBAPR, service=0x6D) +bind_layers(GMLAN, GMLAN_DPBAPR, service=0x6d) +GMLAN._service_cls[0x6d] = GMLAN_DPBAPR # ########################RD################################### class GMLAN_RD(Packet): name = 'RequestDownload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, GMLAN.services), _gmlan_slm), XByteField('dataFormatIdentifier', 0), MultipleTypeField( [ @@ -549,6 +626,7 @@ class GMLAN_RD(Packet): bind_layers(GMLAN, GMLAN_RD, service=0x34) +GMLAN._service_cls[0x34] = GMLAN_RD # ########################TD################################### @@ -559,6 +637,7 @@ class GMLAN_TD(Packet): } name = 'TransferData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), MultipleTypeField( [ @@ -575,23 +654,27 @@ class GMLAN_TD(Packet): bind_layers(GMLAN, GMLAN_TD, service=0x36) +GMLAN._service_cls[0x36] = GMLAN_TD # ########################WDBI################################### class GMLAN_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), StrField("dataRecord", b'') ] -bind_layers(GMLAN, GMLAN_WDBI, service=0x3B) +bind_layers(GMLAN, GMLAN_WDBI, service=0x3b) +GMLAN._service_cls[0x3b] = GMLAN_WDBI class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers) ] @@ -600,7 +683,8 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7B) +bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7b) +GMLAN._service_cls[0x7b] = GMLAN_WDBIPR # ########################RPSPR################################### @@ -619,11 +703,13 @@ class GMLAN_RPSPR(Packet): } name = 'ReportProgrammedStatePositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xe2, GMLAN.services), _gmlan_slm), ByteEnumField('programmedState', 0, programmedStates), ] -bind_layers(GMLAN, GMLAN_RPSPR, service=0xE2) +bind_layers(GMLAN, GMLAN_RPSPR, service=0xe2) +GMLAN._service_cls[0xe2] = GMLAN_RPSPR # ########################PM################################### @@ -635,11 +721,13 @@ class GMLAN_PM(Packet): } name = 'ProgrammingMode' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xa5, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ] -bind_layers(GMLAN, GMLAN_PM, service=0xA5) +bind_layers(GMLAN, GMLAN_PM, service=0xa5) +GMLAN._service_cls[0xa5] = GMLAN_PM # ########################RDI################################### @@ -651,11 +739,13 @@ class GMLAN_RDI(Packet): } name = 'ReadDiagnosticInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xa9, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] -bind_layers(GMLAN, GMLAN_RDI, service=0xA9) +bind_layers(GMLAN, GMLAN_RDI, service=0xa9) +GMLAN._service_cls[0xa9] = GMLAN_RDI class GMLAN_RDI_BN(Packet): @@ -697,17 +787,20 @@ class GMLAN_RDI_BC(Packet): class GMLAN_DC(Packet): name = 'DeviceControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xae, GMLAN.services), _gmlan_slm), XByteField('CPIDNumber', 0), StrFixedLenField('CPIDControlBytes', b"", 5) ] -bind_layers(GMLAN, GMLAN_DC, service=0xAE) +bind_layers(GMLAN, GMLAN_DC, service=0xae) +GMLAN._service_cls[0xae] = GMLAN_DC class GMLAN_DCPR(Packet): name = 'DeviceControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xee, GMLAN.services), _gmlan_slm), XByteField('CPIDNumber', 0) ] @@ -716,7 +809,8 @@ def answers(self, other): and other.CPIDNumber == self.CPIDNumber -bind_layers(GMLAN, GMLAN_DCPR, service=0xEE) +bind_layers(GMLAN, GMLAN_DCPR, service=0xee) +GMLAN._service_cls[0xee] = GMLAN_DCPR # ########################NRC################################### @@ -739,6 +833,7 @@ class GMLAN_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, GMLAN.services), _gmlan_slm), XByteEnumField('requestServiceId', 0, GMLAN.services), MayEnd(ByteEnumField('returnCode', 0, negativeResponseCodes)), # XXX Is this MayEnd correct? Why is the field below also 0xe3 ? @@ -752,3 +847,4 @@ def answers(self, other): bind_layers(GMLAN, GMLAN_NR, service=0x7f) +GMLAN._service_cls[0x7f] = GMLAN_NR diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 5617c1d7a23..019319ee10f 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -21,16 +21,18 @@ XByteField, XShortEnumField, ) -from scapy.packet import Packet, bind_layers, NoPayload +from scapy.packet import Packet, NoPayload, bind_layers from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread -from scapy.plist import _PacketIterable +from scapy.plist import _PacketIterable # noqa: F401 from scapy.contrib.isotp import ISOTP +from scapy.compat import orb -from typing import ( - Dict, +from typing import ( # noqa: F401 Any, + Dict, + Type, ) @@ -43,7 +45,34 @@ "a negative response 'requestCorrectlyReceived-" "ResponsePending' as answer of a request. \n" "The default value is False.") - conf.contribs['KWP'] = {'treat-response-pending-as-answer': False} + conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} + + +def _kwp_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['KWP']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`KWP` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`KWP` packet, preventing a duplicate + service byte when sub-packets are stacked (``KWP()/KWP_SDS()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['KWP'].get('single_layer_mode', False): + return False + if conf.contribs['KWP'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, KWP) + return True class KWP(ISOTP): @@ -113,13 +142,13 @@ def answers(self, other): if not isinstance(other, type(self)): return False if self.service == 0x7f: - return self.payload.answers(other) + return bool(self.payload.answers(other)) if self.service == (other.service + 0x40): if isinstance(self.payload, NoPayload) or \ isinstance(other.payload, NoPayload): return len(self) <= len(other) else: - return self.payload.answers(other.payload) + return bool(self.payload.answers(other.payload)) return False def hashret(self): @@ -129,6 +158,17 @@ def hashret(self): else: return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (bytes, Any, Any) -> type + """Dispatch to the correct KWP service class in single layer mode.""" + if conf.contribs['KWP'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + # ########################SDS################################### class KWP_SDS(Packet): @@ -140,16 +180,19 @@ class KWP_SDS(Packet): 0x92: 'extendedDiagnosticSession'}) name = 'StartDiagnosticSession' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, KWP.services), _kwp_slm), ByteEnumField('diagnosticSession', 0, diagnosticSessionTypes) ] bind_layers(KWP, KWP_SDS, service=0x10) +KWP._service_cls[0x10] = KWP_SDS class KWP_SDSPR(Packet): name = 'StartDiagnosticSessionPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x50, KWP.services), _kwp_slm), ByteEnumField('diagnosticSession', 0, KWP_SDS.diagnosticSessionTypes), ] @@ -161,6 +204,7 @@ def answers(self, other): bind_layers(KWP, KWP_SDSPR, service=0x50) +KWP._service_cls[0x50] = KWP_SDSPR # ######################### KWP_ER ################################### @@ -171,14 +215,19 @@ class KWP_ER(Packet): 0x82: 'nonvolatileMemoryReset'} name = 'ECUReset' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x11, KWP.services), _kwp_slm), ByteEnumField('resetMode', 0, resetModes) ] bind_layers(KWP, KWP_ER, service=0x11) +KWP._service_cls[0x11] = KWP_ER class KWP_ERPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x51, KWP.services), _kwp_slm), + ] name = 'ECUResetPositiveResponse' def answers(self, other): @@ -187,12 +236,14 @@ def answers(self, other): bind_layers(KWP, KWP_ERPR, service=0x51) +KWP._service_cls[0x51] = KWP_ERPR # ######################### KWP_SA ################################### class KWP_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, KWP.services), _kwp_slm), ByteField('accessMode', 0), ConditionalField(StrField('key', b""), lambda pkt: pkt.accessMode % 2 == 0) @@ -200,11 +251,13 @@ class KWP_SA(Packet): bind_layers(KWP, KWP_SA, service=0x27) +KWP._service_cls[0x27] = KWP_SA class KWP_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, KWP.services), _kwp_slm), ByteField('accessMode', 0), ConditionalField(StrField('seed', b""), lambda pkt: pkt.accessMode % 2 == 1), @@ -217,6 +270,7 @@ def answers(self, other): bind_layers(KWP, KWP_SAPR, service=0x67) +KWP._service_cls[0x67] = KWP_SAPR # ######################### KWP_IOCBLI ################################### @@ -231,6 +285,7 @@ class KWP_IOCBLI(Packet): 0x08: "Long Term Adjustment" } fields_desc = [ + ConditionalField(XByteEnumField('service', 0x30, KWP.services), _kwp_slm), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, inputOutputControlParameters), @@ -239,11 +294,13 @@ class KWP_IOCBLI(Packet): bind_layers(KWP, KWP_IOCBLI, service=0x30) +KWP._service_cls[0x30] = KWP_IOCBLI class KWP_IOCBLIPR(Packet): name = 'InputOutputControlByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x70, KWP.services), _kwp_slm), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, KWP_IOCBLI.inputOutputControlParameters), @@ -257,6 +314,7 @@ def answers(self, other): bind_layers(KWP, KWP_IOCBLIPR, service=0x70) +KWP._service_cls[0x70] = KWP_IOCBLIPR # ######################### KWP_DNMT ################################### @@ -267,14 +325,19 @@ class KWP_DNMT(Packet): } name = 'DisableNormalMessageTransmission' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x28, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 0, responseTypes) ] bind_layers(KWP, KWP_DNMT, service=0x28) +KWP._service_cls[0x28] = KWP_DNMT class KWP_DNMTPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x68, KWP.services), _kwp_slm), + ] name = 'DisableNormalMessageTransmissionPositiveResponse' def answers(self, other): @@ -283,6 +346,7 @@ def answers(self, other): bind_layers(KWP, KWP_DNMTPR, service=0x68) +KWP._service_cls[0x68] = KWP_DNMTPR # ######################### KWP_ENMT ################################### @@ -293,14 +357,19 @@ class KWP_ENMT(Packet): } name = 'EnableNormalMessageTransmission' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x29, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes) ] bind_layers(KWP, KWP_ENMT, service=0x29) +KWP._service_cls[0x29] = KWP_ENMT class KWP_ENMTPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x69, KWP.services), _kwp_slm), + ] name = 'EnableNormalMessageTransmissionPositiveResponse' def answers(self, other): @@ -309,6 +378,7 @@ def answers(self, other): bind_layers(KWP, KWP_ENMTPR, service=0x69) +KWP._service_cls[0x69] = KWP_ENMTPR # ######################### KWP_TP ################################### @@ -319,14 +389,19 @@ class KWP_TP(Packet): } name = 'TesterPresent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3e, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes) ] -bind_layers(KWP, KWP_TP, service=0x3E) +bind_layers(KWP, KWP_TP, service=0x3e) +KWP._service_cls[0x3e] = KWP_TP class KWP_TPPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7e, KWP.services), _kwp_slm), + ] name = 'TesterPresentPositiveResponse' def answers(self, other): @@ -334,7 +409,8 @@ def answers(self, other): return isinstance(other, KWP_TP) -bind_layers(KWP, KWP_TPPR, service=0x7E) +bind_layers(KWP, KWP_TPPR, service=0x7e) +KWP._service_cls[0x7e] = KWP_TPPR # ######################### KWP_CDTCS ################################### @@ -357,6 +433,7 @@ class KWP_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x85, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes), XShortEnumField('groupOfDTC', 0, DTCGroups), ByteEnumField('DTCSettingMode', 0, DTCSettingModes), @@ -364,9 +441,13 @@ class KWP_CDTCS(Packet): bind_layers(KWP, KWP_CDTCS, service=0x85) +KWP._service_cls[0x85] = KWP_CDTCS class KWP_CDTCSPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc5, KWP.services), _kwp_slm), + ] name = 'ControlDTCSettingPositiveResponse' def answers(self, other): @@ -374,7 +455,8 @@ def answers(self, other): return isinstance(other, KWP_CDTCS) -bind_layers(KWP, KWP_CDTCSPR, service=0xC5) +bind_layers(KWP, KWP_CDTCSPR, service=0xc5) +KWP._service_cls[0xc5] = KWP_CDTCSPR # ######################### KWP_ROE ################################### @@ -399,6 +481,7 @@ class KWP_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x86, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes), ByteEnumField('eventWindowTime', 0, eventWindowTimes), MayEnd(ByteEnumField('eventType', 0, eventTypes)), @@ -410,11 +493,13 @@ class KWP_ROE(Packet): bind_layers(KWP, KWP_ROE, service=0x86) +KWP._service_cls[0x86] = KWP_ROE class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc6, KWP.services), _kwp_slm), ByteField("numberOfActivatedEvents", 0), MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), # XXX Is this MayEnd correct? @@ -427,7 +512,8 @@ def answers(self, other): and other.eventType == self.eventType -bind_layers(KWP, KWP_ROEPR, service=0xC6) +bind_layers(KWP, KWP_ROEPR, service=0xc6) +KWP._service_cls[0xc6] = KWP_ROEPR # ######################### KWP_RDBLI ################################### @@ -448,16 +534,19 @@ class KWP_RDBLI(Packet): }) name = 'ReadDataByLocalIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x21, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, localIdentifiers) ] bind_layers(KWP, KWP_RDBLI, service=0x21) +KWP._service_cls[0x21] = KWP_RDBLI class KWP_RDBLIPR(Packet): name = 'ReadDataByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x61, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -468,22 +557,26 @@ def answers(self, other): bind_layers(KWP, KWP_RDBLIPR, service=0x61) +KWP._service_cls[0x61] = KWP_RDBLIPR # ######################### KWP_WDBLI ################################### class KWP_WDBLI(Packet): name = 'WriteDataByLocalIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3b, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] -bind_layers(KWP, KWP_WDBLI, service=0x3B) +bind_layers(KWP, KWP_WDBLI, service=0x3b) +KWP._service_cls[0x3b] = KWP_WDBLI class KWP_WDBLIPR(Packet): name = 'WriteDataByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7b, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -493,7 +586,8 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier -bind_layers(KWP, KWP_WDBLIPR, service=0x7B) +bind_layers(KWP, KWP_WDBLIPR, service=0x7b) +KWP._service_cls[0x7b] = KWP_WDBLIPR # ######################### KWP_RDBI ################################### @@ -501,16 +595,19 @@ class KWP_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x22, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, dataIdentifiers) ] bind_layers(KWP, KWP_RDBI, service=0x22) +KWP._service_cls[0x22] = KWP_RDBI class KWP_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -521,23 +618,27 @@ def answers(self, other): bind_layers(KWP, KWP_RDBIPR, service=0x62) +KWP._service_cls[0x62] = KWP_RDBIPR # ######################### KWP_RMBA ################################### class KWP_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x23, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0) ] bind_layers(KWP, KWP_RMBA, service=0x23) +KWP._service_cls[0x23] = KWP_RMBA class KWP_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x63, KWP.services), _kwp_slm), StrField('dataRecord', b"", fmt="B") ] @@ -547,6 +648,7 @@ def answers(self, other): bind_layers(KWP, KWP_RMBAPR, service=0x63) +KWP._service_cls[0x63] = KWP_RMBAPR # ######################### KWP_DDLI ################################### @@ -559,18 +661,21 @@ class KWP_DDLI(Packet): 0x3: "defineByIdentifier", 0x4: "clearDynamicallyDefinedLocalIdentifier"} fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, KWP.services), _kwp_slm), XByteField('dynamicallyDefineLocalIdentifier', 0), ByteEnumField('definitionMode', 0, definitionModes), StrField('dataRecord', b"", fmt="B") ] -bind_layers(KWP, KWP_DDLI, service=0x2C) +bind_layers(KWP, KWP_DDLI, service=0x2c) +KWP._service_cls[0x2c] = KWP_DDLI class KWP_DDLIPR(Packet): name = 'DynamicallyDefineLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, KWP.services), _kwp_slm), XByteField('dynamicallyDefineLocalIdentifier', 0) ] @@ -580,23 +685,27 @@ def answers(self, other): other.dynamicallyDefineLocalIdentifier == self.dynamicallyDefineLocalIdentifier # noqa: E501 -bind_layers(KWP, KWP_DDLIPR, service=0x6C) +bind_layers(KWP, KWP_DDLIPR, service=0x6c) +KWP._service_cls[0x6c] = KWP_DDLIPR # ######################### KWP_WDBI ################################### class KWP_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2e, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers) ] -bind_layers(KWP, KWP_WDBI, service=0x2E) +bind_layers(KWP, KWP_WDBI, service=0x2e) +KWP._service_cls[0x2e] = KWP_WDBI class KWP_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6e, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -606,25 +715,29 @@ def answers(self, other): and other.identifier == self.identifier -bind_layers(KWP, KWP_WDBIPR, service=0x6E) +bind_layers(KWP, KWP_WDBIPR, service=0x6e) +KWP._service_cls[0x6e] = KWP_WDBIPR # ######################### KWP_WMBA ################################### class KWP_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3d, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0), StrField('dataRecord', b'', fmt="B") ] -bind_layers(KWP, KWP_WMBA, service=0x3D) +bind_layers(KWP, KWP_WMBA, service=0x3d) +KWP._service_cls[0x3d] = KWP_WMBA class KWP_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7d, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0) ] @@ -634,7 +747,8 @@ def answers(self, other): other.memoryAddress == self.memoryAddress -bind_layers(KWP, KWP_WMBAPR, service=0x7D) +bind_layers(KWP, KWP_WMBAPR, service=0x7d) +KWP._service_cls[0x7d] = KWP_WMBAPR # ######################### KWP_CDI ################################### @@ -648,17 +762,20 @@ class KWP_CDI(Packet): } name = 'ClearDiagnosticInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x14, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, DTCGroups) ] bind_layers(KWP, KWP_CDI, service=0x14) +KWP._service_cls[0x14] = KWP_CDI class KWP_CDIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x54, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] @@ -669,23 +786,27 @@ def answers(self, other): bind_layers(KWP, KWP_CDIPR, service=0x54) +KWP._service_cls[0x54] = KWP_CDIPR # ######################### KWP_RSODTC ################################### class KWP_RSODTC(Packet): name = 'ReadStatusOfDiagnosticTroubleCodes' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x17, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] bind_layers(KWP, KWP_RSODTC, service=0x17) +KWP._service_cls[0x17] = KWP_RSODTC class KWP_RSODTCPR(Packet): name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x57, KWP.services), _kwp_slm), ByteField('numberOfDTC', 0), ] @@ -695,6 +816,7 @@ def answers(self, other): bind_layers(KWP, KWP_RSODTCPR, service=0x57) +KWP._service_cls[0x57] = KWP_RSODTCPR # ######################### KWP_RECUI ################################### @@ -716,17 +838,20 @@ class KWP_RECUI(Packet): 0x9F: "ECU Boot Fingerprint" }) fields_desc = [ + ConditionalField(XByteEnumField('service', 0x1a, KWP.services), _kwp_slm), XByteEnumField('localIdentifier', 0, localIdentifiers) ] -bind_layers(KWP, KWP_RECUI, service=0x1A) +bind_layers(KWP, KWP_RECUI, service=0x1a) +KWP._service_cls[0x1a] = KWP_RECUI class KWP_RECUIPR(Packet): name = 'ReadECUIdentificationPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x5a, KWP.services), _kwp_slm), XByteEnumField('localIdentifier', 0, KWP_RECUI.localIdentifiers) ] @@ -736,7 +861,8 @@ def answers(self, other): self.localIdentifier == other.localIdentifier -bind_layers(KWP, KWP_RECUIPR, service=0x5A) +bind_layers(KWP, KWP_RECUIPR, service=0x5a) +KWP._service_cls[0x5a] = KWP_RECUIPR # ######################### KWP_SRBLI ################################### @@ -755,16 +881,19 @@ class KWP_SRBLI(Packet): }) name = 'StartRoutineByLocalIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x31, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, routineLocalIdentifiers) ] bind_layers(KWP, KWP_SRBLI, service=0x31) +KWP._service_cls[0x31] = KWP_SRBLI class KWP_SRBLIPR(Packet): name = 'StartRoutineByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x71, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -776,23 +905,27 @@ def answers(self, other): bind_layers(KWP, KWP_SRBLIPR, service=0x71) +KWP._service_cls[0x71] = KWP_SRBLIPR # ######################### KWP_STRBLI ################################### class KWP_STRBLI(Packet): name = 'StopRoutineByLocalIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x32, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] bind_layers(KWP, KWP_STRBLI, service=0x32) +KWP._service_cls[0x32] = KWP_STRBLI class KWP_STRBLIPR(Packet): name = 'StopRoutineByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x72, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -804,23 +937,27 @@ def answers(self, other): bind_layers(KWP, KWP_STRBLIPR, service=0x72) +KWP._service_cls[0x72] = KWP_STRBLIPR # ######################### KWP_RRRBLI ################################### class KWP_RRRBLI(Packet): name = 'RequestRoutineResultsByLocalIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x33, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] bind_layers(KWP, KWP_RRRBLI, service=0x33) +KWP._service_cls[0x33] = KWP_RRRBLI class KWP_RRRBLIPR(Packet): name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x73, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -832,12 +969,14 @@ def answers(self, other): bind_layers(KWP, KWP_RRRBLIPR, service=0x73) +KWP._service_cls[0x73] = KWP_RRRBLIPR # ######################### KWP_RD ################################### class KWP_RD(Packet): name = 'RequestDownload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -846,11 +985,13 @@ class KWP_RD(Packet): bind_layers(KWP, KWP_RD, service=0x34) +KWP._service_cls[0x34] = KWP_RD class KWP_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x74, KWP.services), _kwp_slm), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -860,12 +1001,14 @@ def answers(self, other): bind_layers(KWP, KWP_RDPR, service=0x74) +KWP._service_cls[0x74] = KWP_RDPR # ######################### KWP_RU ################################### class KWP_RU(Packet): name = 'RequestUpload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x35, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -874,11 +1017,13 @@ class KWP_RU(Packet): bind_layers(KWP, KWP_RU, service=0x35) +KWP._service_cls[0x35] = KWP_RU class KWP_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x75, KWP.services), _kwp_slm), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -888,23 +1033,27 @@ def answers(self, other): bind_layers(KWP, KWP_RUPR, service=0x75) +KWP._service_cls[0x75] = KWP_RUPR # ######################### KWP_TD ################################### class KWP_TD(Packet): name = 'TransferData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, KWP.services), _kwp_slm), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] bind_layers(KWP, KWP_TD, service=0x36) +KWP._service_cls[0x36] = KWP_TD class KWP_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x76, KWP.services), _kwp_slm), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -916,22 +1065,26 @@ def answers(self, other): bind_layers(KWP, KWP_TDPR, service=0x76) +KWP._service_cls[0x76] = KWP_TDPR # ######################### KWP_RTE ################################### class KWP_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x37, KWP.services), _kwp_slm), StrField('transferDataRequestParameter', b"", fmt="B") ] bind_layers(KWP, KWP_RTE, service=0x37) +KWP._service_cls[0x37] = KWP_RTE class KWP_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x77, KWP.services), _kwp_slm), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -941,6 +1094,7 @@ def answers(self, other): bind_layers(KWP, KWP_RTEPR, service=0x77) +KWP._service_cls[0x77] = KWP_RTEPR # ######################### KWP_NR ################################### @@ -970,6 +1124,7 @@ class KWP_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, KWP.services), _kwp_slm), MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), # XXX Is this MayEnd correct? ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) @@ -983,6 +1138,7 @@ def answers(self, other): bind_layers(KWP, KWP_NR, service=0x7f) +KWP._service_cls[0x7f] = KWP_NR # ################################################################## diff --git a/scapy/contrib/automotive/obd/iid/iids.py b/scapy/contrib/automotive/obd/iid/iids.py index 908f5b66d42..5c85c2c4834 100644 --- a/scapy/contrib/automotive/obd/iid/iids.py +++ b/scapy/contrib/automotive/obd/iid/iids.py @@ -6,11 +6,14 @@ # scapy.contrib.status = skip -from scapy.fields import FieldLenField, FieldListField, StrFixedLenField, \ - ByteField, ShortField, FlagsField, XByteField, PacketListField +from scapy.fields import ( + ConditionalField, FieldLenField, FieldListField, StrFixedLenField, + ByteField, ShortField, FlagsField, XByteEnumField, XByteField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S09 +from scapy.contrib.automotive.obd.services import OBD_S09, _OBD_SERVICES, _obd_slm # See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_09 @@ -26,6 +29,7 @@ class OBD_S09_PR_Record(Packet): class OBD_S09_PR(Packet): name = "Infotype IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x49, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S09_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/mid/mids.py b/scapy/contrib/automotive/obd/mid/mids.py index 8aa6b7b5624..0acd4a4df1c 100644 --- a/scapy/contrib/automotive/obd/mid/mids.py +++ b/scapy/contrib/automotive/obd/mid/mids.py @@ -6,11 +6,14 @@ # scapy.contrib.status = skip -from scapy.fields import FlagsField, ScalingField, ByteEnumField, \ - MultipleTypeField, ShortField, ShortEnumField, PacketListField +from scapy.fields import ( + ConditionalField, FlagsField, ScalingField, ByteEnumField, + XByteEnumField, MultipleTypeField, ShortField, ShortEnumField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S06 +from scapy.contrib.automotive.obd.services import OBD_S06, _OBD_SERVICES, _obd_slm def _unit_and_scaling_fields(name): @@ -457,6 +460,7 @@ class OBD_S06_PR_Record(Packet): class OBD_S06_PR(Packet): name = "On-Board monitoring IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x46, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S06_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index a165935d2c3..005264f3559 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -14,10 +14,17 @@ from scapy.contrib.automotive.obd.pid.pids import * from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * -from scapy.packet import bind_layers, NoPayload +from scapy.contrib.automotive.obd.services import _OBD_SERVICES +from scapy.packet import NoPayload, bind_layers from scapy.config import conf from scapy.fields import XByteEnumField from scapy.contrib.isotp import ISOTP +from scapy.compat import orb + +from typing import ( # noqa: F401 + Dict, + Type, +) try: if conf.contribs['OBD']['treat-response-pending-as-answer']: @@ -28,32 +35,13 @@ # "a negative response 'requestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['OBD'] = {'treat-response-pending-as-answer': False} + conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} class OBD(ISOTP): - services = { - 0x01: 'CurrentPowertrainDiagnosticDataRequest', - 0x02: 'PowertrainFreezeFrameDataRequest', - 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', - 0x04: 'ClearResetDiagnosticTroubleCodesRequest', - 0x05: 'OxygenSensorMonitoringTestResultsRequest', - 0x06: 'OnBoardMonitoringTestResultsRequest', - 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', - 0x08: 'ControlOperationRequest', - 0x09: 'VehicleInformationRequest', - 0x0A: 'PermanentDiagnosticTroubleCodesRequest', - 0x41: 'CurrentPowertrainDiagnosticDataResponse', - 0x42: 'PowertrainFreezeFrameDataResponse', - 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', - 0x44: 'ClearResetDiagnosticTroubleCodesResponse', - 0x45: 'OxygenSensorMonitoringTestResultsResponse', - 0x46: 'OnBoardMonitoringTestResultsResponse', - 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', - 0x48: 'ControlOperationResponse', - 0x49: 'VehicleInformationResponse', - 0x4A: 'PermanentDiagnosticTroubleCodesResponse', - 0x7f: 'NegativeResponse'} + services = _OBD_SERVICES name = "On-board diagnostics" @@ -79,26 +67,71 @@ def answers(self, other): return self.payload.answers(other.payload) return False + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct OBD service class in single layer mode.""" + if conf.contribs['OBD'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls -# Service Bindings bind_layers(OBD, OBD_S01, service=0x01) +OBD._service_cls[0x01] = OBD_S01 + bind_layers(OBD, OBD_S02, service=0x02) +OBD._service_cls[0x02] = OBD_S02 + bind_layers(OBD, OBD_S03, service=0x03) +OBD._service_cls[0x03] = OBD_S03 + bind_layers(OBD, OBD_S04, service=0x04) +OBD._service_cls[0x04] = OBD_S04 + bind_layers(OBD, OBD_S06, service=0x06) +OBD._service_cls[0x06] = OBD_S06 + bind_layers(OBD, OBD_S07, service=0x07) +OBD._service_cls[0x07] = OBD_S07 + bind_layers(OBD, OBD_S08, service=0x08) +OBD._service_cls[0x08] = OBD_S08 + bind_layers(OBD, OBD_S09, service=0x09) +OBD._service_cls[0x09] = OBD_S09 + bind_layers(OBD, OBD_S0A, service=0x0A) +OBD._service_cls[0x0A] = OBD_S0A bind_layers(OBD, OBD_S01_PR, service=0x41) +OBD._service_cls[0x41] = OBD_S01_PR + bind_layers(OBD, OBD_S02_PR, service=0x42) +OBD._service_cls[0x42] = OBD_S02_PR + bind_layers(OBD, OBD_S03_PR, service=0x43) +OBD._service_cls[0x43] = OBD_S03_PR + bind_layers(OBD, OBD_S04_PR, service=0x44) +OBD._service_cls[0x44] = OBD_S04_PR + bind_layers(OBD, OBD_S06_PR, service=0x46) +OBD._service_cls[0x46] = OBD_S06_PR + bind_layers(OBD, OBD_S07_PR, service=0x47) +OBD._service_cls[0x47] = OBD_S07_PR + bind_layers(OBD, OBD_S08_PR, service=0x48) +OBD._service_cls[0x48] = OBD_S08_PR + bind_layers(OBD, OBD_S09_PR, service=0x49) +OBD._service_cls[0x49] = OBD_S09_PR + bind_layers(OBD, OBD_S0A_PR, service=0x4A) +OBD._service_cls[0x4A] = OBD_S0A_PR + bind_layers(OBD, OBD_NR, service=0x7F) +OBD._service_cls[0x7F] = OBD_NR diff --git a/scapy/contrib/automotive/obd/pid/pids.py b/scapy/contrib/automotive/obd/pid/pids.py index 6367ef1caa5..9ae039a52e1 100644 --- a/scapy/contrib/automotive/obd/pid/pids.py +++ b/scapy/contrib/automotive/obd/pid/pids.py @@ -7,9 +7,11 @@ # scapy.contrib.status = skip from scapy.packet import Packet, bind_layers -from scapy.fields import PacketListField +from scapy.fields import ConditionalField, PacketListField, XByteEnumField -from scapy.contrib.automotive.obd.services import OBD_S01, OBD_S02 +from scapy.contrib.automotive.obd.services import ( + OBD_S01, OBD_S02, _OBD_SERVICES, _obd_slm +) from scapy.contrib.automotive.obd.pid.pids_00_1F import * from scapy.contrib.automotive.obd.pid.pids_20_3F import * from scapy.contrib.automotive.obd.pid.pids_40_5F import * @@ -27,6 +29,7 @@ class OBD_S01_PR_Record(Packet): class OBD_S01_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x41, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S01_PR_Record) ] @@ -45,6 +48,7 @@ class OBD_S02_PR_Record(Packet): class OBD_S02_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x42, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S02_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/pid/pids_00_1F.py b/scapy/contrib/automotive/obd/pid/pids_00_1F.py index 4cfc483021b..829aac6e5ec 100644 --- a/scapy/contrib/automotive/obd/pid/pids_00_1F.py +++ b/scapy/contrib/automotive/obd/pid/pids_00_1F.py @@ -19,7 +19,7 @@ class OBD_PID00(OBD_Packet): name = "PID_00_PIDsSupported" fields_desc = [ - FlagsField('supported_pids', b'', 32, [ + FlagsField('supported_pids', 0, 32, [ 'PID20', 'PID1F', 'PID1E', @@ -109,7 +109,7 @@ class OBD_PID01(OBD_Packet): class OBD_PID02(OBD_Packet): name = "PID_02_FreezeDtc" fields_desc = [ - PacketField('dtc', b'', OBD_DTC) + PacketField('dtc', None, OBD_DTC) ] @@ -250,7 +250,7 @@ class OBD_PID12(OBD_Packet): class OBD_PID13(OBD_Packet): name = "PID_13_OxygenSensorsPresent" fields_desc = [ - FlagsField('sensors_present', b'', 8, [ + FlagsField('sensors_present', 0, 8, [ 'Bank1Sensor1', 'Bank1Sensor2', 'Bank1Sensor3', diff --git a/scapy/contrib/automotive/obd/services.py b/scapy/contrib/automotive/obd/services.py index f00cf0dd519..57303bc13fb 100644 --- a/scapy/contrib/automotive/obd/services.py +++ b/scapy/contrib/automotive/obd/services.py @@ -7,11 +7,68 @@ # scapy.contrib.status = skip from scapy.fields import ByteField, XByteField, BitEnumField, \ - PacketListField, XBitField, XByteEnumField, FieldListField, FieldLenField + PacketListField, XBitField, XByteEnumField, FieldListField, \ + FieldLenField, ConditionalField from scapy.packet import Packet from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.config import conf +_OBD_SERVICES = { + 0x01: 'CurrentPowertrainDiagnosticDataRequest', + 0x02: 'PowertrainFreezeFrameDataRequest', + 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', + 0x04: 'ClearResetDiagnosticTroubleCodesRequest', + 0x05: 'OxygenSensorMonitoringTestResultsRequest', + 0x06: 'OnBoardMonitoringTestResultsRequest', + 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', + 0x08: 'ControlOperationRequest', + 0x09: 'VehicleInformationRequest', + 0x0A: 'PermanentDiagnosticTroubleCodesRequest', + 0x41: 'CurrentPowertrainDiagnosticDataResponse', + 0x42: 'PowertrainFreezeFrameDataResponse', + 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', + 0x44: 'ClearResetDiagnosticTroubleCodesResponse', + 0x45: 'OxygenSensorMonitoringTestResultsResponse', + 0x46: 'OnBoardMonitoringTestResultsResponse', + 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', + 0x48: 'ControlOperationResponse', + 0x49: 'VehicleInformationResponse', + 0x4A: 'PermanentDiagnosticTroubleCodesResponse', + 0x7f: 'NegativeResponse', +} + + +def _obd_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['OBD']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`OBD` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already an :class:`OBD` packet, preventing a duplicate + service byte when sub-packets are stacked (``OBD()/OBD_S01()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + + .. note:: + OBD service classes live in ``services.py`` which is imported by + ``obd.py``. To avoid a circular import the underlayer class is + identified by its class name (``'OBD'``) rather than by an + ``isinstance`` check. + """ + if not conf.contribs['OBD'].get('single_layer_mode', False): + return False + if conf.contribs['OBD'].get('compatibility_mode', True): + ul = pkt.underlayer + return ul is None or type(ul).__name__ != 'OBD' + return True + class OBD_DTC(OBD_Packet): name = "DiagnosticTroubleCode" @@ -45,6 +102,7 @@ class OBD_NR(Packet): } fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7F, _OBD_SERVICES), _obd_slm), XByteField('request_service_id', 0), XByteEnumField('response_code', 0, responses) ] @@ -58,6 +116,7 @@ def answers(self, other): class OBD_S01(Packet): name = "S1_CurrentData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x01, _OBD_SERVICES), _obd_slm), FieldListField("pid", [], XByteField('', 0)) ] @@ -72,17 +131,22 @@ class OBD_S02_Record(OBD_Packet): class OBD_S02(Packet): name = "S2_FreezeFrameData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x02, _OBD_SERVICES), _obd_slm), PacketListField("requests", [], OBD_S02_Record) ] class OBD_S03(Packet): name = "S3_RequestDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x03, _OBD_SERVICES), _obd_slm), + ] class OBD_S03_PR(Packet): name = "S3_ResponseDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x43, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] @@ -93,10 +157,16 @@ def answers(self, other): class OBD_S04(Packet): name = "S4_ClearDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x04, _OBD_SERVICES), _obd_slm), + ] class OBD_S04_PR(Packet): name = "S4_ClearDTCsPositiveResponse" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x44, _OBD_SERVICES), _obd_slm), + ] def answers(self, other): return isinstance(other, OBD_S04) @@ -105,17 +175,22 @@ def answers(self, other): class OBD_S06(Packet): name = "S6_OnBoardDiagnosticMonitoring" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x06, _OBD_SERVICES), _obd_slm), FieldListField("mid", [], XByteField('', 0)) ] class OBD_S07(Packet): name = "S7_RequestPendingDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x07, _OBD_SERVICES), _obd_slm), + ] class OBD_S07_PR(Packet): name = "S7_ResponsePendingDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x47, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] @@ -127,6 +202,7 @@ def answers(self, other): class OBD_S08(Packet): name = "S8_RequestControlOfSystem" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x08, _OBD_SERVICES), _obd_slm), FieldListField("tid", [], XByteField('', 0)) ] @@ -134,17 +210,22 @@ class OBD_S08(Packet): class OBD_S09(Packet): name = "S9_VehicleInformation" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x09, _OBD_SERVICES), _obd_slm), FieldListField("iid", [], XByteField('', 0)) ] class OBD_S0A(Packet): name = "S0A_RequestPermanentDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x0A, _OBD_SERVICES), _obd_slm), + ] class OBD_S0A_PR(Packet): name = "S0A_ResponsePermanentDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x4A, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] diff --git a/scapy/contrib/automotive/obd/tid/tids.py b/scapy/contrib/automotive/obd/tid/tids.py index 27bda0df58a..cf92b7acd01 100644 --- a/scapy/contrib/automotive/obd/tid/tids.py +++ b/scapy/contrib/automotive/obd/tid/tids.py @@ -6,10 +6,13 @@ # scapy.contrib.status = skip -from scapy.fields import FlagsField, ByteField, ScalingField, PacketListField +from scapy.fields import ( + ConditionalField, FlagsField, ByteField, ScalingField, PacketListField, + XByteEnumField +) from scapy.packet import bind_layers, Packet from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S08 +from scapy.contrib.automotive.obd.services import OBD_S08, _OBD_SERVICES, _obd_slm class _OBD_TID_Voltage(OBD_Packet): @@ -132,6 +135,7 @@ class OBD_S08_PR_Record(Packet): class OBD_S08_PR(Packet): name = "Control Operation IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x48, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S08_PR_Record) ] diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 978bf6a0483..aedd4f5652b 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -19,14 +19,16 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, bind_layers, NoPayload, Raw +from scapy.packet import Packet, NoPayload, Raw, bind_layers +from scapy.compat import orb from scapy.config import conf from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP # Typing imports -from typing import ( +from typing import ( # noqa: F401 Dict, + Type, Union, ) @@ -39,11 +41,41 @@ # "a negative response 'requestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['UDS'] = {'treat-response-pending-as-answer': False} + conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + 'single_layer_mode': False, + 'compatibility_mode': True} conf.debug_dissector = True +def _uds_slm(pkt): + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['UDS']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`UDS` acts as a dispatch layer and returns the + matching service sub-packet directly (e.g. + ``UDS(b'\\x10\\x01')`` → ``UDS_DSC``). Each sub-packet gains its + own ``service`` field so that it can be built and dissected + stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`UDS` packet. This prevents a + duplicate service byte when a sub-packet is stacked on top of a UDS + base layer (``UDS()/UDS_DSC()``). Set to *False* to always emit the + ``service`` byte from the sub-packet regardless of stacking. + """ + if not conf.contribs['UDS'].get('single_layer_mode', False): + return False + if conf.contribs['UDS'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, UDS) + return True + + class UDS(ISOTP): services = ObservableDict( {0x10: 'DiagnosticSessionControl', @@ -111,13 +143,13 @@ def answers(self, other): if other.__class__ != self.__class__: return False if self.service == 0x7f: - return self.payload.answers(other) + return bool(self.payload.answers(other)) if self.service == (other.service + 0x40): if isinstance(self.payload, NoPayload) or \ isinstance(other.payload, NoPayload): return len(self) <= len(other) else: - return self.payload.answers(other.payload) + return bool(self.payload.answers(other.payload)) return False def hashret(self): @@ -126,6 +158,17 @@ def hashret(self): return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: Dict[int, Type[Packet]] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct UDS service class in single layer mode.""" + if conf.contribs['UDS'].get('single_layer_mode', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + # ########################DSC################################### class UDS_DSC(Packet): @@ -138,16 +181,19 @@ class UDS_DSC(Packet): 0x7F: 'ISOSAEReserved'}) name = 'DiagnosticSessionControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x10, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, diagnosticSessionTypes) ] bind_layers(UDS, UDS_DSC, service=0x10) +UDS._service_cls[0x10] = UDS_DSC class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x50, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, UDS_DSC.diagnosticSessionTypes), StrField('sessionParameterRecord', b"") @@ -159,6 +205,7 @@ def answers(self, other): bind_layers(UDS, UDS_DSCPR, service=0x50) +UDS._service_cls[0x50] = UDS_DSCPR # #########################ER################################### @@ -174,16 +221,19 @@ class UDS_ER(Packet): 0x7F: 'ISOSAEReserved'} name = 'ECUReset' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x11, UDS.services), _uds_slm), ByteEnumField('resetType', 0, resetTypes) ] bind_layers(UDS, UDS_ER, service=0x11) +UDS._service_cls[0x11] = UDS_ER class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x51, UDS.services), _uds_slm), ByteEnumField('resetType', 0, UDS_ER.resetTypes), ConditionalField(ByteField('powerDownTime', 0), lambda pkt: pkt.resetType == 0x04) @@ -194,12 +244,14 @@ def answers(self, other): bind_layers(UDS, UDS_ERPR, service=0x51) +UDS._service_cls[0x51] = UDS_ERPR # #########################SA################################### class UDS_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x27, UDS.services), _uds_slm), ByteField('securityAccessType', 0), ConditionalField(StrField('securityAccessDataRecord', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -209,11 +261,13 @@ class UDS_SA(Packet): bind_layers(UDS, UDS_SA, service=0x27) +UDS._service_cls[0x27] = UDS_SA class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x67, UDS.services), _uds_slm), ByteField('securityAccessType', 0), ConditionalField(StrField('securitySeed', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -225,6 +279,7 @@ def answers(self, other): bind_layers(UDS, UDS_SAPR, service=0x67) +UDS._service_cls[0x67] = UDS_SAPR # #########################CC################################### @@ -237,6 +292,7 @@ class UDS_CC(Packet): } name = 'CommunicationControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x28, UDS.services), _uds_slm), ByteEnumField('controlType', 0, controlTypes), BitEnumField('communicationType0', 0, 2, {0: 'ISOSAEReserved', @@ -266,11 +322,13 @@ class UDS_CC(Packet): bind_layers(UDS, UDS_CC, service=0x28) +UDS._service_cls[0x28] = UDS_CC class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x68, UDS.services), _uds_slm), ByteEnumField('controlType', 0, UDS_CC.controlTypes) ] @@ -280,6 +338,7 @@ def answers(self, other): bind_layers(UDS, UDS_CCPR, service=0x68) +UDS._service_cls[0x68] = UDS_CCPR # #########################AUTH################################### @@ -298,13 +357,15 @@ class UDS_AUTH(Packet): } name = "Authentication" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x29, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, subFunctions), ConditionalField(XByteField('communicationConfiguration', 0), lambda pkt: pkt.subFunction in [0x01, 0x02, 0x5]), ConditionalField(XShortField('certificateEvaluationId', 0), lambda pkt: pkt.subFunction == 0x04), - ConditionalField(XStrFixedLenField('algorithmIndicator', 0, length=16), - lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), ConditionalField(FieldLenField('lengthOfCertificateClient', None, fmt="H", length_of='certificateClient'), lambda pkt: pkt.subFunction in [0x01, 0x02]), @@ -356,6 +417,7 @@ class UDS_AUTH(Packet): bind_layers(UDS, UDS_AUTH, service=0x29) +UDS._service_cls[0x29] = UDS_AUTH class UDS_AUTHPR(Packet): @@ -379,10 +441,12 @@ class UDS_AUTHPR(Packet): } name = 'AuthenticationPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x69, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, UDS_AUTH.subFunctions), ByteEnumField('returnValue', 0, authenticationReturnParameterTypes), - ConditionalField(XStrFixedLenField('algorithmIndicator', 0, length=16), - lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), ConditionalField(FieldLenField('lengthOfChallengeServer', None, fmt="H", length_of='challengeServer'), lambda pkt: pkt.subFunction in [0x01, 0x02, 0x05]), @@ -436,22 +500,26 @@ def answers(self, other): bind_layers(UDS, UDS_AUTHPR, service=0x69) +UDS._service_cls[0x69] = UDS_AUTHPR # #########################TP################################### class UDS_TP(Packet): name = 'TesterPresent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3e, UDS.services), _uds_slm), ByteField('subFunction', 0) ] -bind_layers(UDS, UDS_TP, service=0x3E) +bind_layers(UDS, UDS_TP, service=0x3e) +UDS._service_cls[0x3e] = UDS_TP class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7e, UDS.services), _uds_slm), ByteField('zeroSubFunction', 0) ] @@ -459,7 +527,8 @@ def answers(self, other): return isinstance(other, UDS_TP) -bind_layers(UDS, UDS_TPPR, service=0x7E) +bind_layers(UDS, UDS_TPPR, service=0x7e) +UDS._service_cls[0x7e] = UDS_TPPR # #########################ATP################################### @@ -473,6 +542,7 @@ class UDS_ATP(Packet): } name = 'AccessTimingParameter' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x83, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, timingParameterAccessTypes), ConditionalField(StrField('timingParameterRequestRecord', b""), @@ -481,11 +551,13 @@ class UDS_ATP(Packet): bind_layers(UDS, UDS_ATP, service=0x83) +UDS._service_cls[0x83] = UDS_ATP class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc3, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, UDS_ATP.timingParameterAccessTypes), ConditionalField(StrField('timingParameterResponseRecord', b""), @@ -498,7 +570,8 @@ def answers(self, other): self.timingParameterAccessType -bind_layers(UDS, UDS_ATPPR, service=0xC3) +bind_layers(UDS, UDS_ATPPR, service=0xc3) +UDS._service_cls[0xc3] = UDS_ATPPR # #########################SDT################################### @@ -507,6 +580,7 @@ def answers(self, other): class UDS_SDT(Packet): name = 'SecuredDataTransmission' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x84, UDS.services), _uds_slm), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -523,11 +597,13 @@ class UDS_SDT(Packet): bind_layers(UDS, UDS_SDT, service=0x84) +UDS._service_cls[0x84] = UDS_SDT class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc4, UDS.services), _uds_slm), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -546,7 +622,8 @@ def answers(self, other): return isinstance(other, UDS_SDT) -bind_layers(UDS, UDS_SDTPR, service=0xC4) +bind_layers(UDS, UDS_SDTPR, service=0xc4) +UDS._service_cls[0xc4] = UDS_SDTPR # #########################CDTCS################################### @@ -558,17 +635,20 @@ class UDS_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x85, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, DTCSettingTypes), StrField('DTCSettingControlOptionRecord', b"") ] bind_layers(UDS, UDS_CDTCS, service=0x85) +UDS._service_cls[0x85] = UDS_CDTCS class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc5, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, UDS_CDTCS.DTCSettingTypes) ] @@ -576,7 +656,8 @@ def answers(self, other): return isinstance(other, UDS_CDTCS) -bind_layers(UDS, UDS_CDTCSPR, service=0xC5) +bind_layers(UDS, UDS_CDTCSPR, service=0xc5) +UDS._service_cls[0xc5] = UDS_CDTCSPR # #########################ROE################################### @@ -588,6 +669,7 @@ class UDS_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x86, UDS.services), _uds_slm), ByteEnumField('eventType', 0, eventTypes), ByteField('eventWindowTime', 0), StrField('eventTypeRecord', b"") @@ -595,11 +677,13 @@ class UDS_ROE(Packet): bind_layers(UDS, UDS_ROE, service=0x86) +UDS._service_cls[0x86] = UDS_ROE class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc6, UDS.services), _uds_slm), ByteEnumField('eventType', 0, UDS_ROE.eventTypes), ByteField('numberOfIdentifiedEvents', 0), ByteField('eventWindowTime', 0), @@ -611,7 +695,8 @@ def answers(self, other): and other.eventType == self.eventType -bind_layers(UDS, UDS_ROEPR, service=0xC6) +bind_layers(UDS, UDS_ROEPR, service=0xc6) +UDS._service_cls[0xc6] = UDS_ROEPR # #########################LC################################### @@ -624,6 +709,7 @@ class UDS_LC(Packet): } name = 'LinkControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x87, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, linkControlTypes), ConditionalField(ByteField('baudrateIdentifier', 0), lambda pkt: pkt.linkControlType == 0x1), @@ -637,11 +723,13 @@ class UDS_LC(Packet): bind_layers(UDS, UDS_LC, service=0x87) +UDS._service_cls[0x87] = UDS_LC class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0xc7, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, UDS_LC.linkControlTypes) ] @@ -650,7 +738,8 @@ def answers(self, other): and other.linkControlType == self.linkControlType -bind_layers(UDS, UDS_LCPR, service=0xC7) +bind_layers(UDS, UDS_LCPR, service=0xc7) +UDS._service_cls[0xc7] = UDS_LCPR # #########################RDBI################################### @@ -658,6 +747,7 @@ class UDS_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x22, UDS.services), _uds_slm), FieldListField("identifiers", None, XShortEnumField('dataIdentifier', 0, dataIdentifiers)) @@ -665,11 +755,13 @@ class UDS_RDBI(Packet): bind_layers(UDS, UDS_RDBI, service=0x22) +UDS._service_cls[0x22] = UDS_RDBI class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x62, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -680,12 +772,14 @@ def answers(self, other): bind_layers(UDS, UDS_RDBIPR, service=0x62) +UDS._service_cls[0x62] = UDS_RDBIPR # #########################RMBA################################### class UDS_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x23, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -708,11 +802,13 @@ class UDS_RMBA(Packet): bind_layers(UDS, UDS_RMBA, service=0x23) +UDS._service_cls[0x23] = UDS_RMBA class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x63, UDS.services), _uds_slm), StrField('dataRecord', b"", fmt="B") ] @@ -721,6 +817,7 @@ def answers(self, other): bind_layers(UDS, UDS_RMBAPR, service=0x63) +UDS._service_cls[0x63] = UDS_RMBAPR # #########################RSDBI################################### @@ -728,17 +825,20 @@ class UDS_RSDBI(Packet): name = 'ReadScalingDataByIdentifier' dataIdentifiers = ObservableDict() fields_desc = [ + ConditionalField(XByteEnumField('service', 0x24, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, dataIdentifiers) ] bind_layers(UDS, UDS_RSDBI, service=0x24) +UDS._service_cls[0x24] = UDS_RSDBI # TODO: Implement correct scaling here, instead of using just the dataRecord class UDS_RSDBIPR(Packet): name = 'ReadScalingDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x64, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RSDBI.dataIdentifiers), ByteField('scalingByte', 0), StrField('dataRecord', b"", fmt="B") @@ -750,6 +850,7 @@ def answers(self, other): bind_layers(UDS, UDS_RSDBIPR, service=0x64) +UDS._service_cls[0x64] = UDS_RSDBIPR # #########################RDBPI################################### @@ -764,19 +865,22 @@ class UDS_RDBPI(Packet): } name = 'ReadDataByPeriodicIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2a, UDS.services), _uds_slm), ByteEnumField('transmissionMode', 0, transmissionModes), ByteEnumField('periodicDataIdentifier', 0, periodicDataIdentifiers), StrField('furtherPeriodicDataIdentifier', b"", fmt="B") ] -bind_layers(UDS, UDS_RDBPI, service=0x2A) +bind_layers(UDS, UDS_RDBPI, service=0x2a) +UDS._service_cls[0x2a] = UDS_RDBPI # TODO: Implement correct scaling here, instead of using just the dataRecord class UDS_RDBPIPR(Packet): name = 'ReadDataByPeriodicIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6a, UDS.services), _uds_slm), ByteField('periodicDataIdentifier', 0), StrField('dataRecord', b"", fmt="B") ] @@ -786,7 +890,8 @@ def answers(self, other): and other.periodicDataIdentifier == self.periodicDataIdentifier -bind_layers(UDS, UDS_RDBPIPR, service=0x6A) +bind_layers(UDS, UDS_RDBPIPR, service=0x6a) +UDS._service_cls[0x6a] = UDS_RDBPIPR # #########################DDDI################################### @@ -798,17 +903,20 @@ class UDS_DDDI(Packet): 0x2: "defineByMemoryAddress", 0x3: "clearDynamicallyDefinedDataIdentifier"} fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, subFunctions), StrField('dataRecord', b"", fmt="B") ] -bind_layers(UDS, UDS_DDDI, service=0x2C) +bind_layers(UDS, UDS_DDDI, service=0x2c) +UDS._service_cls[0x2c] = UDS_DDDI class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, UDS_DDDI.subFunctions), XShortField('dynamicallyDefinedDataIdentifier', 0) ] @@ -818,24 +926,28 @@ def answers(self, other): and other.subFunction == self.subFunction -bind_layers(UDS, UDS_DDDIPR, service=0x6C) +bind_layers(UDS, UDS_DDDIPR, service=0x6c) +UDS._service_cls[0x6c] = UDS_DDDIPR # #########################WDBI################################### class UDS_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers) ] -bind_layers(UDS, UDS_WDBI, service=0x2E) +bind_layers(UDS, UDS_WDBI, service=0x2e) +UDS._service_cls[0x2e] = UDS_WDBI class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -845,13 +957,15 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(UDS, UDS_WDBIPR, service=0x6E) +bind_layers(UDS, UDS_WDBIPR, service=0x6e) +UDS._service_cls[0x6e] = UDS_WDBIPR # #########################WMBA################################### class UDS_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x3d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -875,12 +989,14 @@ class UDS_WMBA(Packet): ] -bind_layers(UDS, UDS_WMBA, service=0x3D) +bind_layers(UDS, UDS_WMBA, service=0x3d) +UDS._service_cls[0x3d] = UDS_WMBA class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -907,13 +1023,14 @@ def answers(self, other): and other.memoryAddressLen == self.memoryAddressLen -bind_layers(UDS, UDS_WMBAPR, service=0x7D) +bind_layers(UDS, UDS_WMBAPR, service=0x7d) +UDS._service_cls[0x7d] = UDS_WMBAPR # ##########################DTC##################################### class DTC(Packet): name = 'Diagnostic Trouble Code' - dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM + dtc_descriptions = {} # type: Dict[int, str] fields_desc = [ BitEnumField("system", 0, 2, { @@ -938,6 +1055,7 @@ def extract_padding(self, s): class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x14, UDS.services), _uds_slm), ByteField('groupOfDTCHighByte', 0), ByteField('groupOfDTCMiddleByte', 0), ByteField('groupOfDTCLowByte', 0), @@ -945,9 +1063,13 @@ class UDS_CDTCI(Packet): bind_layers(UDS, UDS_CDTCI, service=0x14) +UDS._service_cls[0x14] = UDS_CDTCI class UDS_CDTCIPR(Packet): + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x54, UDS.services), _uds_slm), + ] name = 'ClearDiagnosticInformationPositiveResponse' def answers(self, other): @@ -955,6 +1077,7 @@ def answers(self, other): bind_layers(UDS, UDS_CDTCIPR, service=0x54) +UDS._service_cls[0x54] = UDS_CDTCIPR # #########################RDTCI################################### @@ -1012,6 +1135,7 @@ class UDS_RDTCI(Packet): } name = 'ReadDTCInformation' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x19, UDS.services), _uds_slm), ByteEnumField('reportType', 0, reportTypes), ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), lambda pkt: pkt.reportType in [0x07, 0x08]), @@ -1029,6 +1153,7 @@ class UDS_RDTCI(Packet): bind_layers(UDS, UDS_RDTCI, service=0x19) +UDS._service_cls[0x19] = UDS_RDTCI class DTCAndStatusRecord(Packet): @@ -1062,7 +1187,7 @@ class DTCExtendedDataRecord(Packet): class DTCSnapshot(Packet): - identifiers = defaultdict(list) # for later extension + identifiers = defaultdict(list) # type: Dict[int, list] # for later extension @staticmethod def next_identifier_cb(pkt, lst, cur, remain): @@ -1091,6 +1216,7 @@ class DTCSnapshotRecord(Packet): class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x59, UDS.services), _uds_slm), ByteEnumField('reportType', 0, UDS_RDTCI.reportTypes), ConditionalField( FlagsField('DTCStatusAvailabilityMask', 0, 8, UDS_RDTCI.dtcStatus), @@ -1140,6 +1266,7 @@ def answers(self, other): bind_layers(UDS, UDS_RDTCIPR, service=0x59) +UDS._service_cls[0x59] = UDS_RDTCIPR # #########################RC################################### @@ -1153,17 +1280,20 @@ class UDS_RC(Packet): routineControlIdentifiers = ObservableDict() name = 'RoutineControl' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x31, UDS.services), _uds_slm), ByteEnumField('routineControlType', 0, routineControlTypes), XShortEnumField('routineIdentifier', 0, routineControlIdentifiers) ] bind_layers(UDS, UDS_RC, service=0x31) +UDS._service_cls[0x31] = UDS_RC class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x71, UDS.services), _uds_slm), ByteEnumField('routineControlType', 0, UDS_RC.routineControlTypes), XShortEnumField('routineIdentifier', 0, UDS_RC.routineControlIdentifiers), @@ -1181,6 +1311,7 @@ def answers(self, other): bind_layers(UDS, UDS_RCPR, service=0x71) +UDS._service_cls[0x71] = UDS_RCPR # #########################RD################################### @@ -1190,6 +1321,7 @@ class UDS_RD(Packet): }) name = 'RequestDownload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x34, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), @@ -1213,11 +1345,13 @@ class UDS_RD(Packet): bind_layers(UDS, UDS_RD, service=0x34) +UDS._service_cls[0x34] = UDS_RD class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x74, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1228,12 +1362,14 @@ def answers(self, other): bind_layers(UDS, UDS_RDPR, service=0x74) +UDS._service_cls[0x74] = UDS_RDPR # #########################RU################################### class UDS_RU(Packet): name = 'RequestUpload' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x35, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, UDS_RD.dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), @@ -1258,11 +1394,13 @@ class UDS_RU(Packet): bind_layers(UDS, UDS_RU, service=0x35) +UDS._service_cls[0x35] = UDS_RU class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x75, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1273,23 +1411,27 @@ def answers(self, other): bind_layers(UDS, UDS_RUPR, service=0x75) +UDS._service_cls[0x75] = UDS_RUPR # #########################TD################################### class UDS_TD(Packet): name = 'TransferData' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x36, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), StrField('transferRequestParameterRecord', b"", fmt="B") ] bind_layers(UDS, UDS_TD, service=0x36) +UDS._service_cls[0x36] = UDS_TD class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x76, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1300,22 +1442,26 @@ def answers(self, other): bind_layers(UDS, UDS_TDPR, service=0x76) +UDS._service_cls[0x76] = UDS_TDPR # #########################RTE################################### class UDS_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x37, UDS.services), _uds_slm), StrField('transferRequestParameterRecord', b"", fmt="B") ] bind_layers(UDS, UDS_RTE, service=0x37) +UDS._service_cls[0x37] = UDS_RTE class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x77, UDS.services), _uds_slm), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1324,6 +1470,7 @@ def answers(self, other): bind_layers(UDS, UDS_RTEPR, service=0x77) +UDS._service_cls[0x77] = UDS_RTEPR # #########################RFT################################### @@ -1344,6 +1491,7 @@ def _contains_file_size(packet): return packet.modeOfOperation not in [2, 4, 5] fields_desc = [ + ConditionalField(XByteEnumField('service', 0x38, UDS.services), _uds_slm), XByteEnumField('modeOfOperation', 0, modeOfOperations), FieldLenField('filePathAndNameLength', None, length_of='filePathAndName', fmt='H'), @@ -1369,6 +1517,7 @@ def _contains_file_size(packet): bind_layers(UDS, UDS_RFT, service=0x38) +UDS._service_cls[0x38] = UDS_RFT class UDS_RFTPR(Packet): @@ -1379,6 +1528,7 @@ def _contains_data_format_identifier(packet): return packet.modeOfOperation != 0x02 fields_desc = [ + ConditionalField(XByteEnumField('service', 0x78, UDS.services), _uds_slm), XByteEnumField('modeOfOperation', 0, UDS_RFT.modeOfOperations), ConditionalField(FieldLenField('lengthFormatIdentifier', None, length_of='maxNumberOfBlockLength', @@ -1411,22 +1561,26 @@ def answers(self, other): bind_layers(UDS, UDS_RFTPR, service=0x78) +UDS._service_cls[0x78] = UDS_RFTPR # #########################IOCBI################################### class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x2f, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] -bind_layers(UDS, UDS_IOCBI, service=0x2F) +bind_layers(UDS, UDS_IOCBI, service=0x2f) +UDS._service_cls[0x2f] = UDS_IOCBI class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x6f, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1435,7 +1589,8 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(UDS, UDS_IOCBIPR, service=0x6F) +bind_layers(UDS, UDS_IOCBIPR, service=0x6f) +UDS._service_cls[0x6f] = UDS_IOCBIPR # #########################NR################################### @@ -1505,6 +1660,7 @@ class UDS_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7f, UDS.services), _uds_slm), XByteEnumField('requestServiceId', 0, UDS.services), ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] @@ -1516,6 +1672,7 @@ def answers(self, other): bind_layers(UDS, UDS_NR, service=0x7f) +UDS._service_cls[0x7f] = UDS_NR # ################################################################## diff --git a/scapy/contrib/bfd.py b/scapy/contrib/bfd.py index a06a80bd9c2..1c694d28dcf 100644 --- a/scapy/contrib/bfd.py +++ b/scapy/contrib/bfd.py @@ -131,7 +131,7 @@ class BFD(Packet): BitField("echo_rx_interval", 1000000000, 32), ConditionalField( PacketField("optional_auth", None, OptionalAuth), - lambda pkt: pkt.flags.names[2] == "A", + lambda pkt: pkt.flags.A, ), ] diff --git a/scapy/contrib/cansocket_python_can.py b/scapy/contrib/cansocket_python_can.py index 340104a4abb..6f483610e4d 100644 --- a/scapy/contrib/cansocket_python_can.py +++ b/scapy/contrib/cansocket_python_can.py @@ -23,7 +23,7 @@ from scapy.supersocket import SuperSocket from scapy.layers.can import CAN from scapy.packet import Packet -from scapy.error import warning +from scapy.error import warning, log_runtime from typing import ( List, Type, @@ -41,6 +41,36 @@ __all__ = ["CANSocket", "PythonCANSocket"] +# Interfaces with hardware or kernel-level CAN filtering. +# These keep bus-level filters for efficiency since the device/driver +# handles filtering before frames reach userspace. +# All other interfaces (USB adapters like candle, gs_usb, cantact; +# serial like slcan) only do software filtering in BusABC.recv() and +# need filters cleared at the bus level to avoid starving matching +# frames behind non-matching ones (echo frames, other bus traffic). +_HW_FILTERED_INTERFACES = frozenset([ + 'socketcan', 'kvaser', 'vector', 'pcan', 'ixxat', + 'nican', 'neovi', 'etas', 'systec', 'nixnet', +]) + + +def _is_sw_filtered(interface_key): + # type: (str) -> bool + """Return True if the bus identified by interface_key only does + software filtering (inside BusABC.recv). + + For such interfaces, bus-level filters must be cleared so that + bus.recv(timeout=0) returns ALL frames. Per-socket filtering + is then handled by distribute() via _matches_filters(). + + Without this, BusABC.recv(timeout=0) on software-filtered buses + (candle, gs_usb, cantact, slcan, etc.) can silently consume + one non-matching frame per call and return None, starving matching + frames that sit behind it in the USB/serial buffer. + """ + iface = interface_key.split('_')[0].lower() + return iface not in _HW_FILTERED_INTERFACES + class SocketMapper(object): """Internal Helper class to map a python-can bus object to @@ -57,6 +87,14 @@ def __init__(self, bus, sockets): self.bus = bus self.sockets = sockets self.closing = False + # Per-bus lock serializing read_bus() and send_bus(). + # On serial interfaces (slcan) the python-can Bus uses a single + # serial port for both recv() and send(). Without serialization, + # concurrent calls from different threads (TimeoutScheduler for + # recv, main thread for send) corrupt the serial byte stream, + # causing slcan parsing errors. The lock is per-mapper (per-bus) + # so different CAN buses are not blocked by each other. + self.bus_lock = threading.Lock() # Maximum time (seconds) to spend reading frames in one read_bus() # call. On serial interfaces (slcan) the final bus.recv(timeout=0) @@ -76,28 +114,43 @@ def read_bus(self): slcan serial timeout). This method limits total time spent reading so the TimeoutScheduler thread stays responsive. - This method intentionally does NOT hold pool_mutex so that - concurrent send() calls are not blocked during the serial I/O. + This method does NOT hold pool_mutex but DOES hold bus_lock to + serialize with send_bus(). This prevents concurrent serial I/O + on slcan interfaces while still allowing sends to other buses. """ if self.closing: return [] msgs = [] deadline = time.monotonic() + self.READ_BUS_TIME_LIMIT - while True: - try: - msg = self.bus.recv(timeout=0) - if msg is None: + with self.bus_lock: + while True: + try: + msg = self.bus.recv(timeout=0) + if msg is None: + break + else: + msgs.append(msg) + if time.monotonic() >= deadline: + break + except Exception as e: + if not self.closing: + warning("[MUX] python-can exception caught: %s" % e) break - else: - msgs.append(msg) - if time.monotonic() >= deadline: - break - except Exception as e: - if not self.closing: - warning("[MUX] python-can exception caught: %s" % e) - break return msgs + def send_bus(self, msg): + # type: (can_Message) -> None + """Send a CAN message on the bus. + + Serialized with read_bus() via bus_lock to prevent concurrent + serial I/O on slcan interfaces. + + :param msg: python-can Message to send. + :raises can_CanError: If the underlying python-can Bus raises. + """ + with self.bus_lock: + self.bus.send(msg) + def distribute(self, msgs): # type: (List[can_Message]) -> None """Distribute received messages to all subscribed sockets.""" @@ -122,10 +175,10 @@ def internal_send(self, sender, msg): A given SocketWrapper wants to send a CAN message. The python-can Bus object is obtained from an internal pool of SocketMapper objects. - The given message is sent on the python-can Bus object and also - inserted into the message queues of all other SocketWrapper objects - which are connected to the same python-can bus object - by the SocketMapper. + The message is sent on the python-can Bus object via send_bus() + (serialized with read_bus() by bus_lock) and also inserted into + the message queues of all other SocketWrapper objects connected to + the same python-can bus object by the SocketMapper. :param sender: SocketWrapper which initiated a send of a CAN message :param msg: CAN message to be sent @@ -136,30 +189,43 @@ def internal_send(self, sender, msg): with self.pool_mutex: try: mapper = self.pool[sender.name] - mapper.bus.send(msg) - for sock in mapper.sockets: - if sock == sender: - continue - if not sock._matches_filters(msg): - continue - - with sock.lock: - sock.rx_queue.append(msg) except KeyError: warning("[SND] Socket %s not found in pool" % sender.name) - except can_CanError as e: - warning("[SND] python-can exception caught: %s" % e) + return + # Snapshot the peer sockets while under pool_mutex + peers = [s for s in mapper.sockets if s != sender] + + # Send on the bus outside pool_mutex but inside bus_lock. + # This serializes with read_bus() to prevent concurrent serial + # I/O on slcan interfaces, while still allowing multiplexing + # and sends on other buses to proceed concurrently. + try: + mapper.send_bus(msg) + except can_CanError as e: + warning("[SND] python-can exception caught: %s" % e) + return + + # Distribute to peer sockets (no need for pool_mutex here, + # we already have a snapshot of the peers list). + for sock in peers: + if not sock._matches_filters(msg): + continue + with sock.lock: + sock.rx_queue.append(msg) def multiplex_rx_packets(self): # type: () -> None """This calls the mux() function of all SocketMapper objects in this SocketPool """ - if time.monotonic() - self.last_call < 0.001: + now = time.monotonic() + if now - self.last_call < 0.001: # Avoid starvation if multiple threads are doing selects, since # this object is singleton and all python-CAN sockets are using # the same instance and locking the same locks. return + self.last_call = now + # Snapshot pool entries under the lock, then read from each bus # WITHOUT holding pool_mutex. On slow serial interfaces (slcan) # bus.recv(timeout=0) can take ~2-3ms per frame; holding the @@ -171,7 +237,6 @@ def multiplex_rx_packets(self): msgs = mapper.read_bus() if msgs: mapper.distribute(msgs) - self.last_call = time.monotonic() def register(self, socket, *args, **kwargs): # type: (SocketWrapper, Tuple[Any, ...], Dict[str, Any]) -> None @@ -198,12 +263,13 @@ def register(self, socket, *args, **kwargs): t = self.pool[k] t.sockets.append(socket) # Update bus-level filters to the union of all sockets' - # filters. For non-slcan interfaces (socketcan, kvaser, - # vector), this enables efficient hardware/kernel - # filtering. For slcan, the bus filters were already - # cleared on creation, so this is a no-op (all sockets - # on slcan share the unfiltered bus). - if not k.lower().startswith('slcan'): + # filters. For hardware-filtered interfaces (socketcan, + # kvaser, vector, pcan, ixxat), this enables efficient + # kernel/device filtering. For software-filtered + # interfaces (slcan, candle, gs_usb, cantact), the bus + # filters were already cleared on creation, so this is + # a no-op (all sockets share the unfiltered bus). + if not _is_sw_filtered(k): filters = [s.filters for s in t.sockets if s.filters is not None] if filters: @@ -211,21 +277,23 @@ def register(self, socket, *args, **kwargs): socket.name = k else: bus = can_Bus(*args, **kwargs) - # Serial interfaces like slcan only do software - # filtering inside BusABC.recv(): the recv loop reads - # one frame, finds it doesn't match, and returns - # None -- silently consuming serial bandwidth without - # returning the frame to the mux. This starves the - # mux on busy buses. + # Software-filtered interfaces (slcan, candle, gs_usb, + # cantact, etc.) only filter inside BusABC.recv(): the + # recv loop reads one frame, finds it doesn't match, + # and returns None -- silently consuming serial/USB + # bandwidth without returning the frame to the mux. + # On USB adapters with timeout=0 mapped to ~1ms reads, + # this means only one non-matching frame is consumed + # per poll cycle, starving matching ECU responses that + # sit behind echo frames in the hardware buffer. # - # For slcan, clear the filters from the bus so that - # bus.recv() returns ALL frames. Per-socket filtering - # in distribute() via _matches_filters() handles - # delivery. Other interfaces (socketcan, kvaser, - # vector, candle) perform efficient hardware/kernel - # filtering and should keep their bus-level filters. - if kwargs.get('can_filters') and \ - k.lower().startswith('slcan'): + # For all software-filtered interfaces, clear the bus + # filters so bus.recv() returns ALL frames. Per-socket + # filtering in distribute() via _matches_filters() + # handles delivery. Hardware-filtered interfaces + # (socketcan, kvaser, vector, pcan, ixxat) keep their + # bus-level filters for efficiency. + if kwargs.get('can_filters') and _is_sw_filtered(k): bus.set_filters(None) socket.name = k self.pool[k] = SocketMapper(bus, [socket]) @@ -242,17 +310,25 @@ def unregister(self, socket): if socket.name is None: raise TypeError("SocketWrapper.name should never be None") + mapper_to_shutdown = None with self.pool_mutex: try: t = self.pool[socket.name] t.sockets.remove(socket) if not t.sockets: t.closing = True - t.bus.shutdown() del self.pool[socket.name] + mapper_to_shutdown = t except KeyError: warning("Socket %s already removed from pool" % socket.name) + # Shutdown the bus outside pool_mutex. Acquire bus_lock to + # wait for any in-progress read_bus() or send_bus() to finish + # before shutting down the underlying transport. + if mapper_to_shutdown is not None: + with mapper_to_shutdown.bus_lock: + mapper_to_shutdown.bus.shutdown() + SocketsPool = _SocketsPool() @@ -341,6 +417,9 @@ def recv_raw(self, x=0xffff): """Returns a tuple containing (cls, pkt_data, time)""" msg = self.can_iface.recv() + if msg is None: + return self.basecls, None, None + hdr = msg.is_extended_id << 31 | msg.is_remote_frame << 30 | \ msg.is_error_frame << 29 | msg.arbitration_id @@ -382,7 +461,13 @@ def select(sockets, remain=conf.recv_poll_rate): :returns: an array of sockets that were selected and the function to be called next to get the packets (i.g. recv) """ + # Move kernel-buffered CAN frames into the per-socket rx_queues + # BEFORE checking which sockets are ready. The previous order + # (check, then multiplex) returned a stale ready-list that did + # not include sockets whose data had just been multiplexed, + # causing a one-iteration delay. SocketsPool.multiplex_rx_packets() + ready_sockets = \ [s for s in sockets if isinstance(s, PythonCANSocket) and len(s.can_iface.rx_queue)] @@ -401,6 +486,13 @@ def close(self): """Closes this socket""" if self.closed: return + # Final poll to ensure all kernel-buffered frames are distributed + # to any shared socket instances before we unregister. + try: + SocketsPool.multiplex_rx_packets() + except Exception: + log_runtime.debug("Exception during SocketsPool multiplex in close", + exc_info=True) super(PythonCANSocket, self).close() self.can_iface.shutdown() diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index e6baa1dbbf7..a5818bc1b67 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -13,7 +13,7 @@ import time import traceback from bisect import bisect_left -from threading import Thread, Event, RLock +from threading import Thread, Event, RLock, current_thread # Typing imports from typing import ( Optional, @@ -253,8 +253,10 @@ def schedule(cls, timeout, callback): heapq.heappush(cls._handles, handle) must_interrupt = cls._handles[0] == handle - # Start the scheduling thread if it is not started already - if cls._thread is None: + # Start the scheduling thread if it is not started already. + # Also recover if the thread reference is stale (thread died + # without clearing _thread — e.g. from a BaseException). + if cls._thread is None or not cls._thread.is_alive(): t = Thread(target=cls._task, name="TimeoutScheduler._task") t.daemon = True must_interrupt = False @@ -357,17 +359,45 @@ def _task(cls): time_empty = now # 100 ms of grace time before killing the thread if cls.GRACE < now - time_empty: - return + # Atomically check whether new handles arrived + # while we were deciding to die. schedule() + # holds _mutex when it checks _thread, so by + # holding _mutex here we ensure that either: + # (a) _handles is still empty → we set + # _thread = None and return, OR + # (b) a new handle was pushed → we stay alive. + # This closes the race window where schedule() + # saw _thread as not-None but the thread was + # about to die. + with cls._mutex: + if not cls._handles: + cls.logger.debug( + "Thread died @ %f", cls._time()) + cls._thread = None + return + # New handle(s) appeared — stay alive + time_empty = None + continue else: time_empty = None cls._wait(handle) cls._poll() - + except Exception: + cls.logger.exception( + "Thread died @ %f (exception)", cls._time()) finally: - # Worst case scenario: if this thread dies, the next scheduled - # timeout will start a new one - cls.logger.debug("Thread died @ %f", cls._time()) - cls._thread = None + # Clear _thread so the next schedule() call can start a + # fresh thread. Only clear if _thread still points to + # *this* thread; if schedule() already started a + # replacement thread between the normal-exit mutex release + # and this finally block, we must not overwrite the new + # reference. The normal-exit path (GRACE expiry) sets + # _thread = None inside the mutex before returning; this + # finally covers unexpected exits (exceptions, + # BaseException subclasses like SystemExit, etc.). + with cls._mutex: + if cls._thread is current_thread(): + cls._thread = None @classmethod def _poll(cls): @@ -551,10 +581,26 @@ def __init__(self, self.rx_tx_poll_rate = 0.005 self.tx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 self.rx_timeout_handle = None # type: Optional[TimeoutScheduler.Handle] # noqa: E501 + + # Drain frames that accumulated in the CAN adapter's hardware + # RX buffer while no ISOTP socket was active. USB adapters + # (candle, cantact) have small hardware buffers; if background + # CAN traffic fills the buffer before can_recv starts polling, + # the ECU's response frame may be silently dropped by the + # adapter. This drain frees buffer space *before* we send. + try: + self.can_socket.select([self.can_socket], 0) + except Exception: + log_isotp.debug("Exception during ISOTP socket drain select", + exc_info=True) + + # Schedule initial callbacks with timeout=0 so they fire on + # the very next TimeoutScheduler._poll() cycle, minimising + # the window during which the adapter buffer is unserviced. self.rx_handle = TimeoutScheduler.schedule( - self.rx_tx_poll_rate, self.can_recv) + 0, self.can_recv) self.tx_handle = TimeoutScheduler.schedule( - self.rx_tx_poll_rate, self._send) + 0, self._send) self.last_rx_call = 0.0 self.rx_start_time = 0.0 @@ -598,9 +644,22 @@ def _get_padding_size(pl_size): def can_recv(self): # type: () -> None + # Early exit for orphan callbacks: when close() races with + # can_recv on the TimeoutScheduler thread, the old handle may + # fire one last time after closed is set. Without this guard + # the orphan callback would consume CAN frames from the shared + # bus — frames that belong to the NEXT ISOTPSocket session. + if self.closed: + return self.last_rx_call = TimeoutScheduler._time() try: while self.can_socket.select([self.can_socket], 0): + # Re-check closed inside the loop: if close() set the + # flag while we were processing the previous frame, + # stop immediately to avoid consuming frames that + # belong to the next session. + if self.closed: + break pkt = self.can_socket.recv() if pkt: self.on_can_recv(pkt) @@ -642,20 +701,32 @@ def on_can_recv(self, p): def close(self): # type: () -> None + if self.closed: + return + + # Set closed flag FIRST to prevent orphan callbacks from + # consuming CAN frames meant for the next ISOTP session. + # The can_recv() and _send() methods check self.closed at + # entry AND inside their loops, so any in-progress callback + # on the TimeoutScheduler thread will exit promptly. + self.closed = True + + # Brief barrier: yield to the TimeoutScheduler thread so any + # currently-executing callback sees self.closed and exits. + time.sleep(0.005) + + # Diagnostic warnings (non-blocking) try: if select_objects([self.tx_queue], 0): log_isotp.warning("TX queue not empty") - time.sleep(0.1) - except OSError: + except (OSError, Scapy_Exception): pass - try: if select_objects([self.rx_queue], 0): log_isotp.warning("RX queue not empty") - except OSError: + except (OSError, Scapy_Exception): pass - self.closed = True try: self.rx_handle.cancel() except Scapy_Exception: @@ -674,6 +745,19 @@ def close(self): self.tx_timeout_handle.cancel() except Scapy_Exception: pass + + # Final drain: move frames from the CAN adapter's hardware + # buffer into the SocketWrapper software queue. This frees + # adapter buffer space so the NEXT ISOTP session's ECU + # response is not dropped due to hardware overflow. The + # frames stay in the SocketWrapper rx_queue (not lost) and + # will be available to the next session's can_recv. + try: + self.can_socket.select([self.can_socket], 0) + except Exception: + log_isotp.debug("Exception during ISOTP socket drain select", + exc_info=True) + try: self.rx_queue.close() except (OSError, EOFError): @@ -1052,6 +1136,9 @@ def begin_send(self, x): def _send(self): # type: () -> None + # Early exit for orphan callbacks (same rationale as can_recv). + if self.closed: + return try: if self.tx_state == ISOTP_IDLE: if select_objects([self.tx_queue], 0): diff --git a/scapy/contrib/j1939.py b/scapy/contrib/j1939.py new file mode 100644 index 00000000000..e560aadddd0 --- /dev/null +++ b/scapy/contrib/j1939.py @@ -0,0 +1,812 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) 2024 Scapy contributors + +# scapy.contrib.description = SAE J1939 Vehicle Network Protocol +# scapy.contrib.status = loads + +""" +SAE J1939 - Vehicle network protocol for heavy-duty vehicles. + +J1939 uses 29-bit extended CAN identifiers to encode a structured addressing +scheme. The 29-bit identifier is partitioned as follows:: + + Bits 28-26 : Priority (3 bits, 0 = highest) + Bit 25 : Reserved (1 bit) + Bit 24 : Data Page (1 bit) + Bits 23-16 : PDU Format (8 bits, PF) + Bits 15-8 : PDU Specific (8 bits, PS) + PF < 240 → Destination Address (PDU1, peer-to-peer) + PF ≥ 240 → Group Extension (PDU2, broadcast) + Bits 7-0 : Source Address (8 bits, SA) + +PGN (Parameter Group Number): + PDU1 (PF < 240): PGN = (DP << 16) | (PF << 8) — PS is DA + PDU2 (PF ≥ 240): PGN = (DP << 16) | (PF << 8) | GE — broadcast only + +References: + - SAE J1939 standard + - Linux kernel J1939 documentation: + https://www.kernel.org/doc/html/latest/networking/j1939.html +""" + +import socket +import struct +import logging +import time + +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Type, +) + +from scapy.config import conf +from scapy.data import SO_TIMESTAMPNS +from scapy.error import Scapy_Exception, log_runtime +from scapy.fields import ( + BitField, + ByteField, + FieldLenField, + FlagsField, + LEShortField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, + ThreeBytesField, + XLE3BytesField, +) +from scapy.layers.can import CAN +from scapy.packet import Packet +from scapy.supersocket import SuperSocket +from scapy.compat import raw + +log_j1939 = logging.getLogger("scapy.contrib.j1939") + +# --------------------------------------------------------------------------- +# J1939 constants (sourced from Python socket module where available) +# socket.CAN_J1939 and related constants were added in Python 3.9. +# Fallback values are taken from the Linux kernel header linux/can/j1939.h. +# --------------------------------------------------------------------------- + +# Backfill J1939 constants on old Python runtimes (< 3.9) so the module can +# consistently read them from socket.*. +if not hasattr(socket, 'J1939_NO_NAME'): + socket.J1939_NO_NAME = 0 +if not hasattr(socket, 'J1939_NO_PGN'): + socket.J1939_NO_PGN = 0x40000000 +if not hasattr(socket, 'J1939_NO_ADDR'): + socket.J1939_NO_ADDR = 0xFF +if not hasattr(socket, 'J1939_IDLE_ADDR'): + socket.J1939_IDLE_ADDR = 0xFE +if not hasattr(socket, 'J1939_MAX_UNICAST_ADDR'): + socket.J1939_MAX_UNICAST_ADDR = 0xFD +if not hasattr(socket, 'J1939_PGN_REQUEST'): + socket.J1939_PGN_REQUEST = 0xEA00 +if not hasattr(socket, 'J1939_PGN_ADDRESS_CLAIMED'): + socket.J1939_PGN_ADDRESS_CLAIMED = 0xEE00 +if not hasattr(socket, 'J1939_PGN_ADDRESS_COMMANDED'): + socket.J1939_PGN_ADDRESS_COMMANDED = 0xFED8 +if not hasattr(socket, 'J1939_PGN_MAX'): + socket.J1939_PGN_MAX = 0x3FFFF +if not hasattr(socket, 'J1939_PGN_PDU1_MAX'): + socket.J1939_PGN_PDU1_MAX = 0x3FF00 +if not hasattr(socket, 'CAN_J1939'): + socket.CAN_J1939 = 7 + +SOL_CAN_BASE = 100 +if not hasattr(socket, 'SOL_CAN_J1939'): + socket.SOL_CAN_J1939 = SOL_CAN_BASE + socket.CAN_J1939 +if not hasattr(socket, 'SO_J1939_FILTER'): + socket.SO_J1939_FILTER = 1 +if not hasattr(socket, 'SO_J1939_PROMISC'): + socket.SO_J1939_PROMISC = 2 +if not hasattr(socket, 'SO_J1939_SEND_PRIO'): + socket.SO_J1939_SEND_PRIO = 3 +if not hasattr(socket, 'SO_J1939_ERRQUEUE'): + socket.SO_J1939_ERRQUEUE = 4 +if not hasattr(socket, 'SCM_J1939_DEST_ADDR'): + socket.SCM_J1939_DEST_ADDR = 1 +if not hasattr(socket, 'SCM_J1939_DEST_NAME'): + socket.SCM_J1939_DEST_NAME = 2 +if not hasattr(socket, 'SCM_J1939_PRIO'): + socket.SCM_J1939_PRIO = 3 +if not hasattr(socket, 'SCM_J1939_ERRQUEUE'): + socket.SCM_J1939_ERRQUEUE = 4 + +#: Global broadcast address +J1939_BROADCAST_ADDR = socket.J1939_NO_ADDR # 0xFF +#: Transport Protocol – Connection Management +J1939_PGN_TP_CM = 0xEC00 +#: Transport Protocol – Data Transfer +J1939_PGN_TP_DT = 0xEB00 + +# TP control byte values (integer constants; the classes share the prefix name) +J1939_TP_CTRL_RTS = 16 # Request To Send +J1939_TP_CTRL_CTS = 17 # Clear To Send +J1939_TP_CTRL_ACK = 19 # End of Message Acknowledge +J1939_TP_CTRL_BAM = 32 # Broadcast Announce Message +J1939_TP_CTRL_ABORT = 255 # Connection Abort + +# PDU format threshold: PF < 240 → PDU1 (peer-to-peer), PF ≥ 240 → PDU2 (broadcast) +J1939_PDU1_MAX_PF = 239 + +# Default configuration key +conf.contribs['J1939'] = {'channel': 'can0'} + +# Common source address names (informational) +J1939_ADDR_NAMES = { + 0x00: "Engine #1", + 0x01: "Engine #2", + 0x02: "Turbocharger", + 0x03: "Transmission #1", + 0x04: "Transmission #2", + 0x0B: "Brakes - System Controller", + 0x0F: "Instrument Cluster #1", + 0x11: "Trip Recorder", + 0x15: "Retarder, Exhaust, Engine #1", + 0x17: "Cruise Control", + 0x21: "Transmission, Automatic #1", + 0x27: "Clutch/Converter Unit", + 0x28: "Auxiliary Valve Control", + 0x29: "Auxiliary Valve Control #2", + 0xEF: "Null/Reserved", + 0xFE: "NULL (no address)", + 0xFF: "Global (broadcast)", +} + + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def pgn_is_pdu1(pgn): + # type: (int) -> bool + """Return True if *pgn* is a PDU1 (peer-to-peer) Parameter Group Number.""" + return ((pgn >> 8) & 0xFF) <= J1939_PDU1_MAX_PF + + +def can_id_to_j1939(can_id): + # type: (int) -> Dict[str, int] + """Decode a 29-bit CAN identifier to a dictionary of J1939 fields. + + :param can_id: 29-bit extended CAN identifier + :returns: dict with keys ``priority``, ``reserved``, ``data_page``, + ``pdu_format``, ``pdu_specific``, ``src`` + """ + return { + 'priority': (can_id >> 26) & 0x7, + 'reserved': (can_id >> 25) & 0x1, + 'data_page': (can_id >> 24) & 0x1, + 'pdu_format': (can_id >> 16) & 0xFF, + 'pdu_specific': (can_id >> 8) & 0xFF, + 'src': can_id & 0xFF, + } + + +def j1939_to_can_id(priority, reserved, data_page, pdu_format, pdu_specific, src): + # type: (int, int, int, int, int, int) -> int + """Encode J1939 fields into a 29-bit CAN identifier. + + :returns: 29-bit CAN identifier value + """ + return ( + (priority & 0x7) << 26 | + (reserved & 0x1) << 25 | + (data_page & 0x1) << 24 | + (pdu_format & 0xFF) << 16 | + (pdu_specific & 0xFF) << 8 | + (src & 0xFF) + ) + + +def pgn_from_fields(data_page, pdu_format, pdu_specific): + # type: (int, int, int) -> int + """Compute the PGN from J1939 CAN identifier sub-fields. + + :param data_page: data page bit (0 or 1) + :param pdu_format: PDU format byte (0-255) + :param pdu_specific: PDU specific byte (0-255) + :returns: 18-bit PGN value + """ + if pdu_format <= J1939_PDU1_MAX_PF: + # PDU1: PS is destination address – not included in PGN + return (data_page << 16) | (pdu_format << 8) + else: + # PDU2: PS is group extension – included in PGN + return (data_page << 16) | (pdu_format << 8) | pdu_specific + + +def dst_from_fields(pdu_format, pdu_specific): + # type: (int, int) -> int + """Return the destination address encoded in J1939 identifier fields. + + :param pdu_format: PDU format byte (0-255) + :param pdu_specific: PDU specific byte (0-255) + :returns: destination address (0x00-0xFF), or ``socket.J1939_NO_ADDR`` for PDU2 + """ + if pdu_format <= J1939_PDU1_MAX_PF: + return pdu_specific + return socket.J1939_NO_ADDR + + +# --------------------------------------------------------------------------- +# J1939 application-layer packet +# --------------------------------------------------------------------------- + +class J1939(Packet): + """SAE J1939 application-layer message. + + This class represents a J1939 message at the application layer. When used + with :class:`NativeJ1939Socket`, the Linux kernel J1939 stack handles + transport-protocol framing (segmentation / reassembly) automatically, so + ``data`` may be larger than 8 bytes. + + Addressing information – ``priority``, ``pgn``, ``src``, ``dst`` – is + stored in :attr:`__slots__` rather than as wire fields (the same approach + used by :class:`~scapy.contrib.isotp.ISOTP`). These attributes are + populated by :class:`NativeJ1939Socket` upon reception. + + Example:: + + >>> msg = J1939(b'\\x01\\x02\\x03', pgn=0xFECA, src=0x00, dst=0xFF) + >>> msg.pgn + 65226 + >>> msg.src + 0 + """ + + name = 'J1939' + fields_desc = [ + StrField('data', b'') + ] + __slots__ = Packet.__slots__ + ['priority', 'pgn', 'src', 'dst'] + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + self.priority = kwargs.pop('priority', 6) # type: int + self.pgn = kwargs.pop('pgn', 0) # type: int + self.src = kwargs.pop('src', socket.J1939_NO_ADDR) # type: int + self.dst = kwargs.pop('dst', socket.J1939_NO_ADDR) # type: int + Packet.__init__(self, *args, **kwargs) + + def answers(self, other): + # type: (Packet) -> int + if not isinstance(other, J1939): + return 0 + return self.data == other.data + + def mysummary(self): + # type: () -> str + # Addressing is in __slots__, not wire fields, so build the summary directly. + return "J1939 PGN=0x%05X SA=0x%02X DA=0x%02X prio=%d" % ( + self.pgn, self.src, self.dst, self.priority + ) + + +# --------------------------------------------------------------------------- +# J1939 CAN-frame-level packet +# --------------------------------------------------------------------------- + +class J1939_CAN(CAN): + """J1939 CAN frame – the 29-bit extended CAN identifier decoded as J1939. + + Inherits from :class:`~scapy.layers.can.CAN` so that all CAN lifecycle + methods are reused automatically: + + * ``pre_dissect`` / ``post_build`` – byte-order swap controlled by + ``conf.contribs['CAN']['swap-bytes']`` (Wireshark vs PF_CAN format). + * ``extract_padding`` – padding removal controlled by + ``conf.contribs['CAN']['remove-padding']``. + + The only structural difference from :class:`~scapy.layers.can.CAN` is + that the 29-bit ``identifier`` field is decomposed into the six J1939 + sub-fields (``priority``, ``reserved``, ``data_page``, ``pdu_format``, + ``pdu_specific``, ``src``), while the wire layout remains **identical**. + + CAN identifier sub-fields:: + + priority (bits 28-26): message priority, 0 = highest, 7 = lowest + reserved (bit 25) : reserved, should be 0 + data_page (bit 24) : selects one of two parameter group tables + pdu_format (bits 23-16): determines PDU type (< 240 → PDU1) + pdu_specific(bits 15-8) : DA if PDU1, Group Extension if PDU2 + src (bits 7-0) : source address + + Convenience properties :attr:`pgn` and :attr:`dst` are derived from the + sub-fields. + + Example:: + + >>> pkt = J1939_CAN(priority=6, pdu_format=0xFE, pdu_specific=0xCA, + ... src=0x00, data=b'\\xff' * 8) + >>> hex(pkt.pgn) + '0xfeca' + >>> hex(pkt.dst) + '0xff' + """ + + name = 'J1939_CAN' + fields_desc = [ + # ── first 32 bits: CAN flags(3) + J1939 identifier fields(29) ────── + FlagsField('flags', 0b100, 3, + ['error', 'remote_transmission_request', 'extended']), + BitField('priority', 6, 3), # J1939 priority + BitField('reserved', 0, 1), # Reserved bit + BitField('data_page', 0, 1), # Data Page (DP) + ByteField('pdu_format', 0xFE), # PDU Format (PF) + ByteField('pdu_specific', 0xFF), # PDU Specific (PS): DA or GE + ByteField('src', 0xFE), # Source Address (SA) + # ── standard CAN data-length + padding ──────────────────────────── + FieldLenField('length', None, length_of='data', fmt='B'), + ThreeBytesField('reserved2', 0), + StrLenField('data', b'', length_from=lambda p: int(p.length)), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[bytes], *Any, **Any) -> Type[Packet] + # Always decode as J1939_CAN; do not redirect to plain CAN or CANFD. + return cls + + @property + def pgn(self): + # type: () -> int + """PGN (Parameter Group Number) derived from ``data_page``, + ``pdu_format``, and ``pdu_specific``.""" + return pgn_from_fields(self.data_page, self.pdu_format, self.pdu_specific) + + @property + def dst(self): + # type: () -> int + """Destination address for PDU1 frames; :data:`socket.J1939_NO_ADDR` for PDU2.""" # noqa: E501 + return dst_from_fields(self.pdu_format, self.pdu_specific) + + def to_can(self): + # type: () -> CAN + """Convert to a standard :class:`~scapy.layers.can.CAN` packet. + + The wire bytes are identical so this is simply a class change. + """ + return CAN(bytes(self)) + + @classmethod + def from_can(cls, pkt): + # type: (CAN) -> J1939_CAN + """Create a :class:`J1939_CAN` from a :class:`~scapy.layers.can.CAN` packet. + + The wire bytes are identical so this is simply a class change. + The packet timestamp is preserved from *pkt*. + """ + result = cls(bytes(pkt)) + result.time = pkt.time + return result + + def mysummary(self): + # type: () -> str + return "J1939_CAN PGN=0x%05X SA=0x%02X" % (self.pgn, self.src) + + +# --------------------------------------------------------------------------- +# J1939 Transport Protocol (TP) frames +# --------------------------------------------------------------------------- +# TP allows up to 1785 bytes per multi-packet session using PGN 0xEC00 +# (TP.CM – Connection Management) and PGN 0xEB00 (TP.DT – Data Transfer). + +_TP_CM_CTRL_NAMES = { + J1939_TP_CTRL_RTS: 'RTS', + J1939_TP_CTRL_CTS: 'CTS', + J1939_TP_CTRL_ACK: 'EOM_ACK', + J1939_TP_CTRL_BAM: 'BAM', + J1939_TP_CTRL_ABORT: 'ABORT', +} + +_TP_ABORT_REASON = { + 1: 'Already in connection', + 2: 'System resources', + 3: 'Timeout', + 4: 'CTS while DT in progress', + 5: 'Max retransmit exceeded', + 6: 'Unexpected DT packet', + 7: 'Bad sequence number', + 8: 'Duplicate sequence number', + 250: 'Other', + 251: 'Other', + 252: 'Other', + 253: 'Other', + 254: 'Other', + 255: 'Other', +} + + +class J1939_TP_CM_RTS(Packet): + """J1939 TP Connection Management – Request To Send (RTS). + + Sent before a peer-to-peer multi-packet message to announce the total + size and packet count. Uses PGN 0xEC00. + """ + name = 'J1939_TP_CM_RTS' + fields_desc = [ + ByteField('ctrl', J1939_TP_CTRL_RTS), # 16 + LEShortField('total_size', 0), # total message size (bytes) + ByteField('num_packets', 0), # total number of TP.DT packets + ByteField('max_packets', 0xFF), # max packets per CTS (0xFF = no limit) + XLE3BytesField('pgn', 0), # PGN of the message being transferred + ] + + +class J1939_TP_CM_CTS(Packet): + """J1939 TP Connection Management – Clear To Send (CTS). + + Response to :class:`J1939_TP_CM_RTS`; authorises the sender to transmit + up to *num_packets* TP.DT packets starting from *next_packet*. + """ + name = 'J1939_TP_CM_CTS' + fields_desc = [ + ByteField('ctrl', J1939_TP_CTRL_CTS), # 17 + ByteField('num_packets', 0), # number of packets to send now + ByteField('next_packet', 1), # next expected sequence number + ShortField('reserved', 0xFFFF), + XLE3BytesField('pgn', 0), # PGN of the message + ] + + +class J1939_TP_CM_ACK(Packet): + """J1939 TP Connection Management – End of Message Acknowledge (EoMAck). + + Sent by the receiver after all TP.DT packets have been received. + """ + name = 'J1939_TP_CM_ACK' + fields_desc = [ + ByteField('ctrl', J1939_TP_CTRL_ACK), # 19 + LEShortField('total_size', 0), # total message size + ByteField('num_packets', 0), # total TP.DT packets received + ByteField('reserved', 0xFF), + XLE3BytesField('pgn', 0), # PGN of the message + ] + + +class J1939_TP_CM_BAM(Packet): + """J1939 TP Connection Management – Broadcast Announce Message (BAM). + + Announces a forthcoming multi-packet broadcast; no CTS handshake is used. + """ + name = 'J1939_TP_CM_BAM' + fields_desc = [ + ByteField('ctrl', J1939_TP_CTRL_BAM), # 32 + LEShortField('total_size', 0), # total message size (bytes) + ByteField('num_packets', 0), # total number of TP.DT packets + ByteField('reserved', 0xFF), + XLE3BytesField('pgn', 0), # PGN of the message + ] + + +class J1939_TP_CM_ABORT(Packet): + """J1939 TP Connection Management – Connection Abort.""" + name = 'J1939_TP_CM_ABORT' + fields_desc = [ + ByteField('ctrl', J1939_TP_CTRL_ABORT), # 255 + ByteField('reason', 0), # abort reason + ShortField('reserved', 0xFFFF), + ByteField('reserved2', 0xFF), + XLE3BytesField('pgn', 0), # PGN of the aborted message + ] + + +class J1939_TP_CM(Packet): + """J1939 TP Connection Management frame dispatcher. + + Parses a raw 8-byte TP.CM payload and returns the appropriate sub-class. + + Example:: + + >>> J1939_TP_CM(bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00])) + + """ + name = 'J1939_TP_CM' + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + # type: (Optional[bytes], *Any, **Any) -> Type[Packet] + if _pkt and len(_pkt) >= 1: + ctrl = _pkt[0] + if ctrl == J1939_TP_CTRL_RTS: + return J1939_TP_CM_RTS + elif ctrl == J1939_TP_CTRL_CTS: + return J1939_TP_CM_CTS + elif ctrl == J1939_TP_CTRL_ACK: + return J1939_TP_CM_ACK + elif ctrl == J1939_TP_CTRL_BAM: + return J1939_TP_CM_BAM + elif ctrl == J1939_TP_CTRL_ABORT: + return J1939_TP_CM_ABORT + return cls + + def do_dissect(self, s): + # type: (bytes) -> bytes + return s + + +class J1939_TP_DT(Packet): + """J1939 TP Data Transfer frame. + + Each TP.DT packet carries up to 7 bytes of payload; the first byte is the + sequence number (1–255). Unused bytes are padded with ``0xFF``. + """ + name = 'J1939_TP_DT' + fields_desc = [ + ByteField('seq_num', 1), # sequence number 1-255 + StrFixedLenField('data', b'\xff' * 7, 7), # 7 data bytes (0xFF = unused) + ] + + +# --------------------------------------------------------------------------- +# NativeJ1939Socket +# --------------------------------------------------------------------------- + +class NativeJ1939Socket(SuperSocket): + """Linux kernel J1939 socket (``PF_CAN / SOCK_DGRAM / CAN_J1939``). + + The kernel J1939 stack handles transport-protocol framing automatically: + messages larger than 8 bytes are fragmented / reassembled transparently, + and the application deals only with complete J1939 messages. + + .. note:: Design – why not inherit from ``NativeCANSocket``? + + :class:`~scapy.contrib.cansocket_native.NativeCANSocket` uses + ``SOCK_RAW / CAN_RAW``, while this class uses + ``SOCK_DGRAM / CAN_J1939``. The socket type, protocol, ``send()`` + logic (``sendto`` with 4-tuple destination vs plain ``send``), + ``recv()`` logic (``recvmsg`` for J1939 ancillary data vs raw bytes + + byte-order swap), and address binding API are all fundamentally + different. Inheriting from ``NativeCANSocket`` would override or + bypass every method, making the hierarchy misleading rather than + helpful. + + :param channel: CAN interface name (default: ``can0``) + :param src_name: 64-bit J1939 NAME of this node (0 = no name) + :param src_addr: Source address to bind to (:data:`socket.J1939_NO_ADDR` for + promiscuous reception of all addresses) + :param pgn: PGN to bind to (:data:`socket.J1939_NO_PGN` for all PGNs) + :param promisc: Enable promiscuous mode – receive all J1939 messages on + the interface regardless of destination address + :param filters: Optional list of ``j1939_filter`` dicts; each may + contain the keys ``name``, ``name_mask``, ``pgn``, + ``pgn_mask``, ``addr``, ``addr_mask`` + :param basecls: Packet class used to wrap received payloads + (default: :class:`J1939`) + + Example – sniff all J1939 traffic on *vcan0*:: + + >>> sock = NativeJ1939Socket("vcan0", promisc=True) + >>> pkt = sock.recv() + >>> print(pkt.pgn, pkt.src, pkt.data) + + Example – send a J1939 message:: + + >>> sock = NativeJ1939Socket("vcan0", src_addr=0x00) + >>> sock.send(J1939(data=b'\\x01\\x02', pgn=0xFECA, dst=0xFF)) + """ + + desc = "read/write J1939 messages using Linux kernel PF_CAN/CAN_J1939 sockets" + + # struct j1939_filter: name(Q=8) name_mask(Q=8) pgn(I=4) pgn_mask(I=4) addr(B=1) addr_mask(B=1) # noqa: E501 + # Packed size of the 6 fields = 8+8+4+4+1+1 = 26 bytes. + # sizeof(struct j1939_filter) = 32 bytes on 64-bit Linux: the compiler adds 6 bytes of # noqa: E501 + # trailing padding so that the struct size is a multiple of the largest member alignment # noqa: E501 + # (__u64, 8 bytes). The padding must be included when passing an array to setsockopt(2). # noqa: E501 + _J1939_FILTER_FMT = "=QQIIBB" + _J1939_FILTER_PAD = b'\x00' * 6 # 6 bytes padding to reach 32-byte alignment + + def __init__( + self, + channel=None, # type: Optional[str] + src_name=socket.J1939_NO_NAME, # type: int + src_addr=socket.J1939_NO_ADDR, # type: int + pgn=socket.J1939_NO_PGN, # type: int + promisc=True, # type: bool + filters=None, # type: Optional[List[Dict[str, int]]] + basecls=J1939, # type: Type[Packet] + **kwargs # type: Any + ): + # type: (...) -> None + self.channel = channel or conf.contribs['J1939']['channel'] + self.src_name = src_name + self.src_addr = src_addr + self.pgn = pgn + self.basecls = basecls + + self.ins = socket.socket( + socket.PF_CAN, socket.SOCK_DGRAM, socket.CAN_J1939 + ) + + if promisc: + try: + self.ins.setsockopt( + socket.SOL_CAN_J1939, + socket.SO_J1939_PROMISC, + struct.pack('i', 1), + ) + except OSError as exc: + raise Scapy_Exception( + "Could not enable J1939 promiscuous mode: %s" % exc + ) + + # Allow sending and receiving broadcast (global address 0xFF / socket.J1939_NO_ADDR). # noqa: E501 + # The Linux kernel J1939 stack refuses sendto() calls with addr=socket.J1939_NO_ADDR # noqa: E501 + # unless SO_BROADCAST is set, returning EACCES. + try: + self.ins.setsockopt( + socket.SOL_SOCKET, + socket.SO_BROADCAST, + struct.pack('i', 1), + ) + except OSError as exc: + raise Scapy_Exception( + "Could not enable SO_BROADCAST on J1939 socket: %s" % exc + ) + + # Enable ancillary data so we can read destination address and priority + try: + self.ins.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) + self.auxdata_available = True + except OSError: + self.auxdata_available = False + log_runtime.info("SO_TIMESTAMPNS not supported on this kernel") + + if filters is not None: + self._set_filters(filters) + + self.ins.bind((self.channel, src_name, pgn, src_addr)) + self.outs = self.ins + + def _set_filters(self, filters): + # type: (List[Dict[str, int]]) -> None + """Apply a list of J1939 filters to the socket. + + Each filter dict may contain any of: + ``name``, ``name_mask``, ``pgn``, ``pgn_mask``, ``addr``, ``addr_mask``. + """ + packed = b'' + for f in filters: + packed += struct.pack( + self._J1939_FILTER_FMT, + f.get('name', socket.J1939_NO_NAME), + f.get('name_mask', socket.J1939_NO_NAME), + f.get('pgn', socket.J1939_NO_PGN), + f.get('pgn_mask', socket.J1939_NO_PGN), + f.get('addr', socket.J1939_NO_ADDR), + f.get('addr_mask', socket.J1939_NO_ADDR), + ) + self._J1939_FILTER_PAD + try: + self.ins.setsockopt(socket.SOL_CAN_J1939, socket.SO_J1939_FILTER, packed) + except OSError as exc: + raise Scapy_Exception( + "Could not set J1939 filter: %s" % exc + ) + + def recv_raw(self, x=4096): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] + """Returns a tuple ``(cls, pkt_data, timestamp)``. + + .. note:: + The returned *pkt_data* is only the raw J1939 payload bytes. + Addressing metadata (PGN, source/destination address, priority) is + unavailable through this low-level interface; use :meth:`recv` + instead to obtain a fully populated :class:`J1939` packet. + """ + try: + pkt_data = self.ins.recv(x) + except BlockingIOError: + log_j1939.warning('Captured no data, socket in non-blocking mode.') + return None, None, None + except socket.timeout: + log_j1939.warning('Captured no data, socket read timed out.') + return None, None, None + except OSError as exc: + log_j1939.warning('Captured no data: %s', exc) + return None, None, None + + return self.basecls, pkt_data, None + + def recv(self, x=4096, **kwargs): + # type: (int, **Any) -> Optional[Packet] + """Receive one J1939 message, including addressing metadata. + + Returns a :attr:`basecls` instance (default: :class:`J1939`) with + ``priority``, ``pgn``, ``src``, and ``dst`` populated from the kernel. + """ + try: + data, ancdata, _flags, addr = self.ins.recvmsg(x, 256) + except BlockingIOError: + log_j1939.warning('Captured no data, socket in non-blocking mode.') + return None + except socket.timeout: + log_j1939.warning('Captured no data, socket read timed out.') + return None + except OSError as exc: + log_j1939.warning('Captured no data: %s', exc) + return None + + # addr = (iface_name, name, pgn, src_addr) + _iface, _src_name, src_pgn, src_addr = addr + + dst_addr = socket.J1939_NO_ADDR + priority = 6 + ts = None + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level == socket.SOL_CAN_J1939: + if cmsg_type == socket.SCM_J1939_DEST_ADDR: + if cmsg_data: + dst_addr = struct.unpack('B', cmsg_data[:1])[0] + elif cmsg_type == socket.SCM_J1939_PRIO: + if cmsg_data: + priority = struct.unpack('B', cmsg_data[:1])[0] + + if ts is None: + ts = time.time() + + try: + pkt = self.basecls( + data, + priority=priority, + pgn=src_pgn, + src=src_addr, + dst=dst_addr, + ) + except Exception: + pkt = self.basecls(data) + + pkt.time = ts + return pkt + + def send(self, x): + # type: (Packet) -> int + """Send a J1939 message. + + If *x* is a :class:`J1939` packet, the ``pgn``, ``dst``, and + ``priority`` attributes are used. For other packet types the raw bytes + are sent to the socket's default destination. + """ + if x is None: + return 0 + + try: + x.sent_time = time.time() + except AttributeError: + pass + + # Extract payload bytes + if isinstance(x, J1939): + data = x.data if isinstance(x.data, bytes) else raw(x) + dst_pgn = x.pgn if x.pgn != 0 else socket.J1939_NO_PGN + dst_addr = x.dst + priority = x.priority + else: + data = raw(x) + dst_pgn = socket.J1939_NO_PGN + dst_addr = socket.J1939_NO_ADDR + priority = 6 + + # Set per-message priority + try: + self.outs.setsockopt( + socket.SOL_CAN_J1939, + socket.SO_J1939_SEND_PRIO, + struct.pack('i', priority), + ) + except OSError: + pass # not critical + + dst = (self.channel, socket.J1939_NO_NAME, dst_pgn, dst_addr) + try: + return self.outs.sendto(data, dst) + except OSError as exc: + log_j1939.error("Failed to send J1939 packet: %s", exc) + return 0 diff --git a/scapy/contrib/ldp.py b/scapy/contrib/ldp.py index bd08ee8f58f..17a6af2972a 100644 --- a/scapy/contrib/ldp.py +++ b/scapy/contrib/ldp.py @@ -437,7 +437,7 @@ def post_build(self, p, pay): bind_bottom_up(TCP, LDP, sport=646) bind_bottom_up(TCP, LDP, dport=646) -bind_bottom_up(TCP, UDP, sport=646) -bind_bottom_up(TCP, UDP, dport=646) +bind_bottom_up(UDP, LDP, sport=646) +bind_bottom_up(UDP, LDP, dport=646) bind_layers(TCP, LDP, sport=646, dport=646) bind_layers(UDP, LDP, sport=646, dport=646) diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py index 10c2f6a4e92..3e75cef8e88 100644 --- a/scapy/fwdmachine.py +++ b/scapy/fwdmachine.py @@ -341,7 +341,7 @@ def handler(self, sock, addr, dest): # Wrap both server and peer sockets in SSL if self.tls: # Build client SSL context - clisslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + clisslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS) clisslcontext.load_default_certs() clisslcontext.check_hostname = False clisslcontext.verify_mode = ssl.CERT_NONE diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 0122559e5f0..f5259305143 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -58,6 +58,7 @@ LEMACField, BitEnumField, LEThreeBytesField, + ConditionalField ) from scapy.supersocket import SuperSocket from scapy.sendrecv import sndrcv @@ -929,8 +930,12 @@ class SM_Identity_Information(Packet): class SM_Identity_Address_Information(Packet): name = "Identity Address Information" - fields_desc = [ByteEnumField("atype", 0, {0: "public"}), - LEMACField("address", None), ] + fields_desc = [ByteEnumField("addr_type", 0, {0: "public"}), + LEMACField("addr", None), ] + deprecated_fields = { + "atype": ("addr_type", "2.7.0"), + "address": ("addr", "2.7.0"), + } class SM_Signing_Information(Packet): @@ -954,6 +959,17 @@ class SM_DHKey_Check(Packet): fields_desc = [StrFixedLenField("dhkey_check", b'\x00' * 16, 16), ] +class SM_Keypress_Notification(Packet): + name = "Keypress Notification" + fields_desc = [ByteEnumField("notification_type", 0, { + 0: "Passkey entry started", + 1: "Passkey digit entered", + 2: "Passkey digit erased", + 3: "Passkey cleared", + 4: "Passkey entry completed", + })] + + class EIR_Hdr(Packet): name = "EIR Header" fields_desc = [ @@ -1003,6 +1019,8 @@ class EIR_Hdr(Packet): 0x2a: "mesh_message", 0x2b: "mesh_beacon", + 0x30: "broadcast_name", + 0x3d: "3d_information", 0xff: "mfg_specific_data", @@ -1292,6 +1310,13 @@ class EIR_PublicTargetAddress(EIR_Element): ] +class EIR_RandomTargetAddress(EIR_Element): + name = "Random Target Address" + fields_desc = [ + LEMACField('bd_addr', None) + ] + + class EIR_AdvertisingInterval(EIR_Element): name = "Advertising Interval" fields_desc = [ @@ -1323,6 +1348,40 @@ class EIR_LEBluetoothDeviceAddress(EIR_Element): ] +class EIR_LERole(EIR_Element): + name = "LE Role" + fields_desc = [ + ByteEnumField("role", 0, { + 0: "Only Peripheral Role supported", + 1: "Only Central Role supported", + 2: "Peripheral and Central Role supported, " + "Peripheral Role preferred for connection establishment", + 3: "Peripheral and Central Role supported, " + "Central Role preferred for connection establishment", + }), + ] + + +class EIR_BroadcastName(EIR_Element): + name = "Broadcast Name" + fields_desc = [ + StrLenField("broadcast_name", "", + length_from=EIR_Element.length_from) + ] + + +class EIR_3DInformation(EIR_Element): + name = "3D Information" + fields_desc = [ + BitField("factory_test_mode", 0, 1, tot_size=-1), + BitField("reserved", 0, 4), + BitField("send_battery_level_on_startup", 0, 1), + BitField("battery_level_reporting", 0, 1), + BitField("association_notification", 0, 1, end_tot_size=-1), + ByteField("path_loss_threshold", 0), + ] + + class EIR_Appearance(EIR_Element): name = "EIR_Appearance" fields_desc = [ @@ -2045,6 +2104,11 @@ class HCI_Cmd_Write_Loopback_Mode(Packet): # 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 +class HCI_Cmd_LE_Set_Event_Mask(Packet): + name = 'HCI_LE_Set_Event_Mask' + fields_desc = [StrFixedLenField('mask', b'\xff\xff\xff\xff\xff\x1f\x00\x00', 8)] + + class HCI_Cmd_LE_Read_Buffer_Size_V1(Packet): name = "HCI_LE_Read_Buffer_Size [v1]" @@ -2059,19 +2123,74 @@ class HCI_Cmd_LE_Read_Local_Supported_Features(Packet): class HCI_Cmd_LE_Set_Random_Address(Packet): name = "HCI_LE_Set_Random_Address" - fields_desc = [LEMACField("address", None)] + fields_desc = [LEMACField("addr", None)] + deprecated_fields = {"address": ("addr", "2.7.0")} class HCI_Cmd_LE_Set_Advertising_Parameters(Packet): name = "HCI_LE_Set_Advertising_Parameters" fields_desc = [LEShortField("interval_min", 0x0800), LEShortField("interval_max", 0x0800), - ByteEnumField("adv_type", 0, {0: "ADV_IND", 1: "ADV_DIRECT_IND", 2: "ADV_SCAN_IND", 3: "ADV_NONCONN_IND", 4: "ADV_DIRECT_IND_LOW"}), # noqa: E501 - ByteEnumField("oatype", 0, {0: "public", 1: "random"}), - ByteEnumField("datype", 0, {0: "public", 1: "random"}), - LEMACField("daddr", None), + ByteEnumField("adv_type", 0, { + 0: "ADV_IND", + 1: "ADV_DIRECT_IND", + 2: "ADV_SCAN_IND", + 3: "ADV_NONCONN_IND", + 4: "ADV_DIRECT_IND_LOW"}), + ByteEnumField("own_addr_type", 0, { + 0: "public", + 1: "random"}), + ByteEnumField("peer_addr_type", 0, { + 0: "public", + 1: "random"}), + LEMACField("peer_addr", None), ByteField("channel_map", 7), - ByteEnumField("filter_policy", 0, {0: "all:all", 1: "connect:all scan:whitelist", 2: "connect:whitelist scan:all", 3: "all:whitelist"}), ] # noqa: E501 + ByteEnumField("filter_policy", 0, { + 0: "all:all", + 1: "connect:all scan:whitelist", + 2: "connect:whitelist scan:all", + 3: "all:whitelist"}), ] + deprecated_fields = { + "oatype": ("own_addr_type", "2.7.0"), + "datype": ("peer_addr_type", "2.7.0"), + "daddr": ("peer_addr", "2.7.0"), + } + + +class HCI_Cmd_LE_Set_Extended_Advertising_Parameters(Packet): + name = 'HCI_LE_Set_Extended_Advertising_Parameters' + fields_desc = [ByteField('handle', 0), + LEShortField('properties', 19), + LEThreeBytesField('pri_interval_min', 160), + LEThreeBytesField('pri_interval_max', 160), + ByteField('pri_channel_map', 7), + ByteEnumField('own_addr_type', 0, { + 0: 'public', + 1: 'random', + 2: 'rpa_pub', + 3: 'rpa_rand'}), + ByteEnumField('peer_addr_type', 0, { + 0: 'public', + 1: 'random', + 2: 'rpa_pub', + 3: 'rpa_rand'}), + LEMACField('peer_addr', None), + ByteEnumField("filter_policy", 0, { + 0: "all:all", + 1: "connect:all scan:whitelist", + 2: "connect:whitelist scan:all", + 3: "all:whitelist"}), + SignedByteField('tx_power', 127), + ByteEnumField('pri_phy', 1, {1: '1M', 3: 'Coded'}), + ByteField('sec_max_skip', 0), + ByteEnumField('sec_phy', 1, {1: '1M', 2: '2M', 3: 'Coded'}), + ByteField('sid', 0), + ByteField('scan_req_notify_enable', 0)] + + +class HCI_Cmd_LE_Set_Advertising_Set_Random_Address(Packet): + name = 'HCI_LE_Set_Advertising_Set_Random_Address' + fields_desc = [ByteField('handle', 0), LEMACField('addr', None)] class HCI_Cmd_LE_Set_Advertising_Data(Packet): @@ -2083,6 +2202,20 @@ class HCI_Cmd_LE_Set_Advertising_Data(Packet): align=31, padwith=b"\0"), ] +class HCI_Cmd_LE_Set_Extended_Advertising_Data(Packet): + name = 'HCI_LE_Set_Extended_Advertising_Data' + fields_desc = [ByteField('handle', 0), + ByteEnumField('operation', 3, { + 0: 'intermediate_frag', + 1: 'first_frag', + 2: 'last_frag', + 3: 'complete', + 4: 'unchanged_data'}), + ByteEnumField('frag_pref', 1, {0: 'allow_frag', 1: 'no_frag'}), + FieldLenField('len', None, length_of='data', fmt='B'), + PacketListField('data', [], EIR_Hdr, length_from=lambda pkt: pkt.len)] # noqa: E501 + + class HCI_Cmd_LE_Set_Scan_Response_Data(Packet): name = "HCI_LE_Set_Scan_Response_Data" fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), @@ -2094,16 +2227,69 @@ class HCI_Cmd_LE_Set_Advertise_Enable(Packet): fields_desc = [ByteField("enable", 0)] +class Extended_Advertise_Set(Packet): + name = 'Extended Advertising Set' + fields_desc = [ByteField('handle', 0), + LEShortField('duration', 0), + ByteField('max_events', 0)] + + +class HCI_Cmd_LE_Set_Extended_Advertise_Enable(Packet): + name = 'HCI_LE_Set_Extended_Advertising_Enable' + fields_desc = [ByteEnumField('enable', 1, {0: 'disable', 1: 'enable'}), + FieldLenField('num_sets', None, count_of='sets', fmt='B'), + PacketListField('sets', [], Extended_Advertise_Set, count_from=lambda pkt: pkt.num_sets)] # noqa: E501 + + class HCI_Cmd_LE_Set_Scan_Parameters(Packet): name = "HCI_LE_Set_Scan_Parameters" fields_desc = [ByteEnumField("type", 0, {0: "passive", 1: "active"}), XLEShortField("interval", 16), XLEShortField("window", 16), - ByteEnumField("atype", 0, {0: "public", - 1: "random", - 2: "rpa (pub)", - 3: "rpa (random)"}), + ByteEnumField("addr_type", 0, { + 0: "public", + 1: "random", + 2: "rpa (pub)", + 3: "rpa (random)"}), ByteEnumField("policy", 0, {0: "all", 1: "whitelist"})] + deprecated_fields = {"atype": ("addr_type", "2.7.0")} + + +class HCI_Cmd_LE_Set_Extended_Scan_Parameters(Packet): + name = 'HCI_LE_Set_Extended_Scan_Parameters' + fields_desc = [ + ByteEnumField('own_address_type', 0, { + 0: 'public', + 1: 'random', + 2: 'rpa_pub', + 3: 'rpa_rand'}), + ByteEnumField('scanning_filter_policy', 0, { + 0: 'basic', + 1: 'whitelist', + 2: 'basic_rpa', + 3: 'whitelist_rpa'}), + ByteField('scanning_phys', 1), + ConditionalField(ByteEnumField('scan_type_1m', 1, { + 0: 'passive', + 1: 'active'}), lambda pkt: pkt.scanning_phys & 1), + ConditionalField(LEShortField('scan_interval_1m', 16), + lambda pkt: pkt.scanning_phys & 1), + ConditionalField(LEShortField('scan_window_1m', 16), + lambda pkt: pkt.scanning_phys & 1), + ConditionalField(ByteEnumField('scan_type_2m', 1, { + 0: 'passive', + 1: 'active'}), lambda pkt: pkt.scanning_phys & 2), + ConditionalField(LEShortField('scan_interval_2m', 16), + lambda pkt: pkt.scanning_phys & 2), + ConditionalField(LEShortField('scan_window_2m', 16), + lambda pkt: pkt.scanning_phys & 2), + ConditionalField(ByteEnumField('scan_type_coded', 1, { + 0: 'passive', + 1: 'active'}), lambda pkt: pkt.scanning_phys & 4), + ConditionalField(LEShortField('scan_interval_coded', 16), + lambda pkt: pkt.scanning_phys & 4), + ConditionalField(LEShortField('scan_window_coded', 16), + lambda pkt: pkt.scanning_phys & 4)] class HCI_Cmd_LE_Set_Scan_Enable(Packet): @@ -2112,20 +2298,101 @@ class HCI_Cmd_LE_Set_Scan_Enable(Packet): ByteField("filter_dups", 1), ] +class HCI_Cmd_LE_Set_Extended_Scan_Enable(Packet): + name = 'HCI_LE_Set_Extended_Scan_Enable' + fields_desc = [ByteEnumField('enable', 1, {0: 'disabled', 1: 'enabled'}), + ByteEnumField('filter_dups', 1, { + 0: 'disabled', + 1: 'enabled', + 2: 'reset_period'}), + LEShortField('duration', 500), + LEShortField('period', 0)] + + class HCI_Cmd_LE_Create_Connection(Packet): name = "HCI_LE_Create_Connection" fields_desc = [LEShortField("interval", 96), LEShortField("window", 48), ByteEnumField("filter", 0, {0: "address"}), - ByteEnumField("patype", 0, {0: "public", 1: "random"}), - LEMACField("paddr", None), - ByteEnumField("atype", 0, {0: "public", 1: "random"}), + ByteEnumField("peer_addr_type", 0, {0: "public", 1: "random"}), + LEMACField("peer_addr", None), + ByteEnumField("own_addr_type", 0, {0: "public", 1: "random"}), LEShortField("min_interval", 40), LEShortField("max_interval", 56), LEShortField("latency", 0), LEShortField("timeout", 42), LEShortField("min_ce", 0), LEShortField("max_ce", 0), ] + deprecated_fields = { + "patype": ("peer_addr_type", "2.7.0"), + "paddr": ("peer_addr", "2.7.0"), + "atype": ("own_addr_type", "2.7.0"), + } + + +class HCI_Cmd_LE_Extended_Create_Connection(Packet): + name = 'HCI_LE_Extended_Create_Connection' + fields_desc = [ByteEnumField('filter_policy', 0, {0: 'peer_addr', 1: 'accept_list'}), # noqa: E501 + ByteEnumField('address_type', 0, { + 0: 'public', + 1: 'random', + 2: 'rpa_pub', + 3: 'rpa_rand'}), + ByteEnumField('peer_addr_type', 0, { + 0: 'public', + 1: 'random', + 2: 'rpa_pub', + 3: 'rpa_rand'}), + LEMACField('peer_addr', None), + ByteField('phys', 1), + ConditionalField(LEShortField('interval_1m', 96), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('window_1m', 96), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('min_interval_1m', 40), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('max_interval_1m', 56), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('latency_1m', 0), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('timeout_1m', 42), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('min_ce_1m', 0), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('max_ce_1m', 0), + lambda pkt: pkt.phys & 1), + ConditionalField(LEShortField('interval_2m', 96), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('window_2m', 96), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('min_interval_2m', 40), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('max_interval_2m', 56), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('latency_2m', 0), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('timeout_2m', 42), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('min_ce_2m', 0), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('max_ce_2m', 0), + lambda pkt: pkt.phys & 2), + ConditionalField(LEShortField('interval_coded', 96), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('window_coded', 96), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('min_interval_coded', 40), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('max_interval_coded', 56), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('latency_coded', 0), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('timeout_coded', 42), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('min_ce_coded', 0), + lambda pkt: pkt.phys & 4), + ConditionalField(LEShortField('max_ce_coded', 0), + lambda pkt: pkt.phys & 4)] class HCI_Cmd_LE_Create_Connection_Cancel(Packet): @@ -2142,10 +2409,10 @@ class HCI_Cmd_LE_Clear_Filter_Accept_List(Packet): class HCI_Cmd_LE_Add_Device_To_Filter_Accept_List(Packet): name = "HCI_LE_Add_Device_To_Filter_Accept_List" - fields_desc = [ByteEnumField("address_type", 0, {0: "public", + fields_desc = [ByteEnumField("addr_type", 0, {0: "public", 1: "random", 0xff: "anonymous"}), - LEMACField("address", None)] + LEMACField("addr", None)] class HCI_Cmd_LE_Remove_Device_From_Filter_Accept_List(HCI_Cmd_LE_Add_Device_To_Filter_Accept_List): # noqa: E501 @@ -2246,6 +2513,18 @@ class HCI_Event_Connection_Complete(Packet): 1: "link level encryption enabled", }), ] +class HCI_Event_Connection_Request(Packet): + """ + 7.7.4 Connection Request event + """ + name = "HCI_Connection_Request" + fields_desc = [LEMACField("bd_addr", None), + XLE3BytesField("device_class", 0), + ByteEnumField("link_type", 0, {0: "SCO connection", + 1: "ACL connection", + 2: "eSCO connection", }), ] + + class HCI_Event_Disconnection_Complete(Packet): """ 7.7.5 Disconnection Complete event @@ -2288,6 +2567,17 @@ class HCI_Event_Read_Remote_Supported_Features_Complete(Packet): ] +class HCI_Event_Remote_Host_Supported_Features_Notification(Packet): + """ + 7.7.50 Remote Host Supported Features Notification event + """ + name = "HCI_Remote_Host_Supported_Features_Notification" + fields_desc = [ + LEMACField('bd_addr', None), + XLELongField('host_supported_features', 0) + ] + + class HCI_Event_Read_Remote_Version_Information_Complete(Packet): """ 7.7.12 Read Remote Version Information Complete event @@ -2424,6 +2714,19 @@ class HCI_Event_IO_Capability_Response(Packet): ] +class HCI_Event_Vendor(Packet): + """ + Vendor-Specific Debug event (event code 0xFF). + + Bluetooth Core 5.4, Vol 4, Part E, section 5.4.4 reserves 0xFF for + vendor-specific debugging events; the format of the parameters is + vendor-defined, so the data is exposed as a raw byte string. + """ + name = "HCI_Vendor_Specific" + fields_desc = [StrLenField("data", b"", + length_from=lambda pkt: pkt.underlayer.len)] + + class HCI_Event_LE_Meta(Packet): """ 7.7.65 LE Meta event @@ -2506,19 +2809,57 @@ class HCI_LE_Meta_Connection_Complete(Packet): fields_desc = [ByteEnumField("status", 0, {0: "success"}), LEShortField("handle", 0), ByteEnumField("role", 0, {0: "master"}), - ByteEnumField("patype", 0, {0: "public", 1: "random"}), - LEMACField("paddr", None), + ByteEnumField("peer_addr_type", 0, {0: "public", 1: "random"}), + LEMACField("peer_addr", None), LEShortField("interval", 54), LEShortField("latency", 0), LEShortField("supervision", 42), - XByteField("clock_latency", 5), ] + XByteField("master_clock_accuracy", 5)] + deprecated_fields = { + "patype": ("peer_addr_type", "2.7.0"), + "paddr": ("peer_addr", "2.7.0"), + "clock_latency": ("master_clock_accuracy", "2.7.0"), + } + + def answers(self, other): + if HCI_Cmd_LE_Create_Connection in other: + cmd = other[HCI_Cmd_LE_Create_Connection] + elif HCI_Cmd_LE_Extended_Create_Connection in other: + cmd = other[HCI_Cmd_LE_Extended_Create_Connection] + else: + return False + + return (cmd.peer_addr_type == self.peer_addr_type and + cmd.peer_addr == self.peer_addr) + + +class HCI_LE_Meta_Enhanced_Connection_Complete(Packet): + name = 'LE Enhanced Connection Complete' + fields_desc = [ByteEnumField('status', 0, {0: 'success'}), + LEShortField('handle', 0), + ByteEnumField('role', 0, {0: 'master', 1: 'slave'}), + ByteEnumField('peer_addr_type', 0, { + 0: 'public', + 1: 'random', + 2: 'public_identity', + 3: 'random_identity'}), + LEMACField('peer_addr', None), + LEMACField('local_rpa', None), + LEMACField('peer_rpa', None), + LEShortField('interval', 54), + LEShortField('latency', 0), + LEShortField('supervision', 42), + XByteField('master_clock_accuracy', 5)] def answers(self, other): - if HCI_Cmd_LE_Create_Connection not in other: + if HCI_Cmd_LE_Create_Connection in other: + cmd = other[HCI_Cmd_LE_Create_Connection] + elif HCI_Cmd_LE_Extended_Create_Connection in other: + cmd = other[HCI_Cmd_LE_Extended_Create_Connection] + else: return False - return (other[HCI_Cmd_LE_Create_Connection].patype == self.patype and - other[HCI_Cmd_LE_Create_Connection].paddr == self.paddr) + return cmd.peer_addr_type == self.peer_addr_type and cmd.peer_addr == self.peer_addr # noqa: E501 class HCI_LE_Meta_Connection_Update_Complete(Packet): @@ -2530,15 +2871,23 @@ class HCI_LE_Meta_Connection_Update_Complete(Packet): LEShortField("timeout", 42), ] +class HCI_LE_Meta_LE_Read_Remote_Features_Complete(Packet): + name = "LE Read Remote Features Complete" + fields_desc = [ByteEnumField("status", 0, _bluetooth_error_codes), + LEShortField("handle", 0), + XLELongField("le_features", 0)] + + class HCI_LE_Meta_Advertising_Report(Packet): name = "Advertising Report" fields_desc = [ByteEnumField("type", 0, {0: "conn_und", 4: "scan_rsp"}), - ByteEnumField("atype", 0, {0: "public", 1: "random"}), + ByteEnumField("addr_type", 0, {0: "public", 1: "random"}), LEMACField("addr", None), FieldLenField("len", None, length_of="data", fmt="B"), PacketListField("data", [], EIR_Hdr, length_from=lambda pkt: pkt.len), SignedByteField("rssi", 0)] + deprecated_fields = {"atype": ("addr_type", "2.7.0")} def extract_padding(self, s): return '', s @@ -2575,14 +2924,14 @@ class HCI_LE_Meta_Extended_Advertising_Report(Packet): BitField("scannable", 0, 1), BitField("connectable", 0, 1), ByteField("reserved", 0), - ByteEnumField("address_type", 0, { + ByteEnumField("addr_type", 0, { 0x00: "public_device_address", 0x01: "random_device_address", 0x02: "public_identity_address", 0x03: "random_identity_address", 0xff: "anonymous" }), - LEMACField('address', None), + LEMACField('addr', None), ByteEnumField("primary_phy", 0, { 0x01: "le_1m", 0x03: "le_coded_s8", @@ -2598,17 +2947,23 @@ class HCI_LE_Meta_Extended_Advertising_Report(Packet): ByteField("tx_power", 0x7f), SignedByteField("rssi", 0x00), LEShortField("periodic_advertising_interval", 0x0000), - ByteEnumField("direct_address_type", 0, { + ByteEnumField("direct_addr_type", 0, { 0x00: "public_device_address", 0x01: "non_resolvable_private_address", 0x02: "resolvable_private_address_resolved_0", 0x03: "resolvable_private_address_resolved_1", 0xfe: "resolvable_private_address_unable_resolve"}), - LEMACField("direct_address", None), + LEMACField("direct_addr", None), FieldLenField("data_length", None, length_of="data", fmt="B"), PacketListField("data", [], EIR_Hdr, length_from=lambda pkt: pkt.data_length), ] + deprecated_fields = { + "address_type": ("addr_type", "2.7.0"), + "address": ("addr", "2.7.0"), + "direct_address_type": ("direct_addr_type", "2.7.0"), + "direct_address": ("direct_addr", "2.7.0"), + } def extract_padding(self, s): return '', s @@ -2699,18 +3054,26 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Loopback_Mode, ogf=0x06, ocf=0x0002) # 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Event_Mask, ogf=0x08, ocf=0x0001) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V1, ogf=0x08, ocf=0x0002) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V2, ogf=0x08, ocf=0x0060) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Local_Supported_Features, ogf=0x08, ocf=0x0003) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, ogf=0x08, ocf=0x0005) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, ogf=0x08, ocf=0x0006) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Set_Random_Address, ogf=0x08, ocf=0x0035) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Extended_Advertising_Parameters, ogf=0x08, ocf=0x0036) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Data, ogf=0x08, ocf=0x0008) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Extended_Advertising_Data, ogf=0x08, ocf=0x0037) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Response_Data, ogf=0x08, ocf=0x0009) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, ogf=0x08, ocf=0x000a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Extended_Advertise_Enable, ogf=0x08, ocf=0x0039) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, ogf=0x08, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Extended_Scan_Parameters, ogf=0x08, ocf=0x0041) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, ogf=0x08, ocf=0x000c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Extended_Scan_Enable, ogf=0x08, ocf=0x0042) bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, ogf=0x08, ocf=0x000d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Extended_Create_Connection, ogf=0x08, ocf=0x0043) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, ogf=0x08, ocf=0x000e) # noqa: E501 bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Filter_Accept_List_Size, ogf=0x08, ocf=0x000f) @@ -2727,6 +3090,7 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Complete, code=0x01) bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Result, code=0x02) bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Complete, code=0x03) +bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Request, code=0x04) bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x08) @@ -2740,7 +3104,9 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_Hdr, HCI_Event_Read_Remote_Extended_Features_Complete, code=0x23) bind_layers(HCI_Event_Hdr, HCI_Event_Extended_Inquiry_Result, code=0x2f) bind_layers(HCI_Event_Hdr, HCI_Event_IO_Capability_Response, code=0x32) +bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Host_Supported_Features_Notification, code=0x3d) # noqa: E501 bind_layers(HCI_Event_Hdr, HCI_Event_LE_Meta, code=0x3e) +bind_layers(HCI_Event_Hdr, HCI_Event_Vendor, code=0xff) bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Name, opcode=0x0c14) # noqa: E501 bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Version_Information, opcode=0x1001) # noqa: E501 @@ -2749,8 +3115,10 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_LE_Read_White_List_Size, opcode=0x200f) # noqa: E501 bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Complete, event=0x01) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Enhanced_Connection_Complete, event=0x0a) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Advertising_Reports, event=0x02) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Update_Complete, event=0x03) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_LE_Read_Remote_Features_Complete, event=0x04) # noqa: E501 bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Long_Term_Key_Request, event=0x05) bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Extended_Advertising_Reports, event=0x0d) @@ -2774,12 +3142,16 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(EIR_Hdr, EIR_ServiceSolicitation128BitUUID, type=0x15) bind_layers(EIR_Hdr, EIR_ServiceData16BitUUID, type=0x16) bind_layers(EIR_Hdr, EIR_PublicTargetAddress, type=0x17) +bind_layers(EIR_Hdr, EIR_RandomTargetAddress, type=0x18) bind_layers(EIR_Hdr, EIR_Appearance, type=0x19) bind_layers(EIR_Hdr, EIR_AdvertisingInterval, type=0x1a) bind_layers(EIR_Hdr, EIR_LEBluetoothDeviceAddress, type=0x1b) +bind_layers(EIR_Hdr, EIR_LERole, type=0x1c) bind_layers(EIR_Hdr, EIR_ServiceData32BitUUID, type=0x20) bind_layers(EIR_Hdr, EIR_ServiceData128BitUUID, type=0x21) bind_layers(EIR_Hdr, EIR_URI, type=0x24) +bind_layers(EIR_Hdr, EIR_BroadcastName, type=0x30) +bind_layers(EIR_Hdr, EIR_3DInformation, type=0x3d) bind_layers(EIR_Hdr, EIR_Manufacturer_Specific_Data, type=0xff) bind_layers(EIR_Hdr, EIR_Raw) @@ -2854,6 +3226,7 @@ class HCI_LE_Meta_Extended_Advertising_Reports(Packet): bind_layers(SM_Hdr, SM_Security_Request, sm_command=0x0b) bind_layers(SM_Hdr, SM_Public_Key, sm_command=0x0c) bind_layers(SM_Hdr, SM_DHKey_Check, sm_command=0x0d) +bind_layers(SM_Hdr, SM_Keypress_Notification, sm_command=0x0e) ############### @@ -2986,7 +3359,7 @@ def build_advertising_report(self): return HCI_LE_Meta_Advertising_Report( type=0, # Undirected - atype=1, # Random address + addr_type=1, # Random address data=self.build_eir() ) @@ -3156,8 +3529,8 @@ def send_command(self, cmd): self.send(cmd) while True: r = self.recv() - if r.type == 0x04 and r.code == 0xe and r.opcode == opcode: - if r.status != 0: + if r.type == 0x04 and r.code in (0xe, 0xf) and r.opcode == opcode: + if hasattr(r, 'status') and r.status != 0: raise BluetoothCommandError("Command %x failed with %x" % (opcode, r.status)) # noqa: E501 return r diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index a79b90b9ce8..c8876a4a30d 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -3351,14 +3351,18 @@ def __init__(self, *args, **kwargs): ) super(DceRpcSocket, self).__init__(*args, **kwargs) - def send(self, x, **kwargs): + def send(self, x, is_sr1=False, **kwargs): for pkt in self.session.out_pkt(x): if self.transport == DCERPC_Transport.NCACN_NP: # In this case DceRpcSocket wraps a SMB_RPC_SOCKET, call it directly. - self.ins.send(pkt, **kwargs) + self.ins.send(pkt, is_sr1=is_sr1, **kwargs) else: super(DceRpcSocket, self).send(pkt, **kwargs) + def sr1(self, *args, **kwargs): + # We allow to use IOCTL only when sr1() is used, as we expect an answer. + return super(DceRpcSocket, self).sr1(*args, is_sr1=True, **kwargs) + def recv(self, x=None): pkt = super(DceRpcSocket, self).recv(x) if pkt is not None: diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py index 3177ee38186..a45044c5532 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -494,11 +494,11 @@ class EDNS0OWN(_EDNS0Dummy): MACField("primary_mac", "00:00:00:00:00:00"), ConditionalField( MACField("wakeup_mac", "00:00:00:00:00:00"), - lambda pkt: (pkt.optlen or 0) >= 18), + lambda pkt: (pkt.optlen or 0) >= 14), ConditionalField( StrLenField("password", "", - length_from=lambda pkt: pkt.optlen - 18), - lambda pkt: (pkt.optlen or 0) >= 22)] + length_from=lambda pkt: pkt.optlen - 14), + lambda pkt: (pkt.optlen or 0) >= 18)] def post_build(self, pkt, pay): pkt += pay diff --git a/scapy/layers/dot15d4.py b/scapy/layers/dot15d4.py index b83933de6ef..c733c3d23b4 100644 --- a/scapy/layers/dot15d4.py +++ b/scapy/layers/dot15d4.py @@ -221,6 +221,9 @@ class Dot15d4AuxSecurityHeader(Packet): lambda pkt: pkt.getfieldval("sec_sc_keyidmode") != 0), ] + def extract_padding(self, s): + return b"", s + class Dot15d4Data(Packet): name = "802.15.4 Data" @@ -233,10 +236,14 @@ class Dot15d4Data(Packet): lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501 # Security field present if fcf_security == True ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501 - lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501 + lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501 ] def guess_payload_class(self, payload): + # Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further + if self.aux_sec_header is not None and \ + self.aux_sec_header.sec_sc_seclevel >= 4: + return conf.raw_layer # TODO: See how it's done in wireshark: # https://github.com/wireshark/wireshark/blob/93c60b3b7c801dddd11d8c7f2a0ea4b7d02d700a/epan/dissectors/packet-ieee802154.c#L2061 # noqa: E501 # it's too magic to me @@ -268,7 +275,7 @@ class Dot15d4Beacon(Packet): dot15d4AddressField("src_addr", None, length_of="fcf_srcaddrmode"), # Security field present if fcf_security == True ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501 - lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501 + lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501 # Superframe spec field: BitField("sf_sforder", 15, 4), # not used by ZigBee @@ -306,6 +313,13 @@ class Dot15d4Beacon(Packet): # TODO beacon payload ] + def guess_payload_class(self, payload): + # Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further + if self.aux_sec_header is not None and \ + self.aux_sec_header.sec_sc_seclevel >= 4: + return conf.raw_layer + return Packet.guess_payload_class(self, payload) + def mysummary(self): return self.sprintf("802.15.4 Beacon ( %Dot15d4Beacon.src_panid%:%Dot15d4Beacon.src_addr% ) assocPermit(%Dot15d4Beacon.sf_assocpermit%) panCoord(%Dot15d4Beacon.sf_pancoord%)") # noqa: E501 @@ -323,7 +337,7 @@ class Dot15d4Cmd(Packet): lambda pkt:pkt.underlayer.getfieldval("fcf_srcaddrmode") != 0), # noqa: E501 # Security field present if fcf_security == True ConditionalField(PacketField("aux_sec_header", Dot15d4AuxSecurityHeader(), Dot15d4AuxSecurityHeader), # noqa: E501 - lambda pkt:pkt.underlayer.getfieldval("fcf_security") is True), # noqa: E501 + lambda pkt:pkt.underlayer.getfieldval("fcf_security")), # noqa: E501 ByteEnumField("cmd_id", 0, { 1: "AssocReq", # Association request 2: "AssocResp", # Association response @@ -345,6 +359,10 @@ def mysummary(self): # command frame payloads are complete: DataReq, PANIDConflictNotify, OrphanNotify, BeaconReq don't have any payload # noqa: E501 # Although BeaconReq can have an optional ZigBee Beacon payload (implemented in ZigBeeBeacon) # noqa: E501 def guess_payload_class(self, payload): + # Encrypted payloads (sec_sc_seclevel >= 4) cannot be dissected further + if self.aux_sec_header is not None and \ + self.aux_sec_header.sec_sc_seclevel >= 4: + return conf.raw_layer if self.cmd_id == 1: return Dot15d4CmdAssocReq elif self.cmd_id == 2: diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py index 6f4d5e91343..5e1732ab078 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -342,7 +342,7 @@ def fromssl( log_runtime.warning("Failed to parse the SSL Certificate. CBT not used") return GSS_C_NO_CHANNEL_BINDINGS try: - h = cert.getSignatureHash() + h = cert.getCertSignatureHash() except Exception: # We failed to get the signature algorithm. log_runtime.warning( diff --git a/scapy/layers/hsrp.py b/scapy/layers/hsrp.py index 82e82606357..b64b9425d16 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -13,25 +13,47 @@ """ from scapy.config import conf -from scapy.fields import ByteEnumField, ByteField, IPField, SourceIPField, \ - StrFixedLenField, XIntField, XShortField +from scapy.compat import orb +from scapy.fields import ByteEnumField, ByteField, IntField, IPField, \ + ShortEnumField, ShortField, SourceIPField, StrFixedLenField, \ + XIntField, XShortField from scapy.packet import Packet, bind_layers, bind_bottom_up from scapy.layers.inet import DestIPField, UDP +_HSRP_OPCODES = {0: "Hello", 1: "Coup", 2: "Resign", 3: "Advertise"} +_HSRP_STATES = { + 0: "Initial", + 1: "Learn", + 2: "Listen", + 4: "Speak", + 8: "Standby", + 16: "Active", +} +_HSRP_ADVERTISE_TYPES = {1: "HSRP interface state"} +_HSRP_ADVERTISE_STATES = {1: "Active", 2: "Passive"} + + class HSRP(Packet): name = "HSRP" fields_desc = [ ByteField("version", 0), - ByteEnumField("opcode", 0, {0: "Hello", 1: "Coup", 2: "Resign", 3: "Advertise"}), # noqa: E501 - ByteEnumField("state", 16, {0: "Initial", 1: "Learn", 2: "Listen", 4: "Speak", 8: "Standby", 16: "Active"}), # noqa: E501 + ByteEnumField("opcode", 0, _HSRP_OPCODES), + ByteEnumField("state", 16, _HSRP_STATES), ByteField("hellotime", 3), ByteField("holdtime", 10), ByteField("priority", 120), ByteField("group", 1), ByteField("reserved", 0), StrFixedLenField("auth", b"cisco" + b"\00" * 3, 8), - IPField("virtualIP", "192.168.1.1")] + IPField("virtualIP", "192.168.1.1") + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2 and orb(_pkt[1:2]) == 3: + return HSRPAdvertise + return cls def guess_payload_class(self, payload): if self.underlayer.len > 28: @@ -40,6 +62,27 @@ def guess_payload_class(self, payload): return Packet.guess_payload_class(self, payload) +class HSRPAdvertise(Packet): + name = "HSRP Advertise" + fields_desc = [ + ByteField("version", 0), + ByteEnumField("opcode", 3, _HSRP_OPCODES), + ShortEnumField("type", 1, _HSRP_ADVERTISE_TYPES), + ShortField("length", 10), + ByteEnumField("state", 1, _HSRP_ADVERTISE_STATES), + ByteField("reserved1", 0), + ShortField("activegroups", 0), + ShortField("passivegroups", 0), + IntField("reserved2", 0), + ] + + @classmethod + def dispatch_hook(cls, _pkt=None, *args, **kargs): + if _pkt and len(_pkt) >= 2 and orb(_pkt[1:2]) != 3: + return HSRP + return cls + + class HSRPmd5(Packet): name = "HSRP MD5 Authentication" fields_desc = [ diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index 0540d17e3d4..f8e18ce64c2 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -269,15 +269,21 @@ def toString(self): return "/".join(x.val.decode() for x in self.nameString) @staticmethod - def fromUPN(upn: str): + def fromUPN(upn: str, canonicalize: bool = False): """ Create a PrincipalName from a UPN string. """ - user, _ = _parse_upn(upn) - return PrincipalName( - nameString=[ASN1_GENERAL_STRING(user)], - nameType=ASN1_INTEGER(1), # NT-PRINCIPAL - ) + if canonicalize: + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(upn)], + nameType=ASN1_INTEGER(10), # NT-ENTERPRISE + ) + else: + user, _ = _parse_upn(upn) + return PrincipalName( + nameString=[ASN1_GENERAL_STRING(user)], + nameType=ASN1_INTEGER(1), # NT-PRINCIPAL + ) @staticmethod def fromSPN(spn: str): @@ -728,6 +734,7 @@ class AD_AND_OR(ASN1_Packet): 15: "PA-PK-AS-REP-OLD", 16: "PA-PK-AS-REQ", 17: "PA-PK-AS-REP", + 18: "PA-PK-OCSP-RESPONSE", 19: "PA-ETYPE-INFO2", 20: "PA-SVR-REFERRAL-INFO", 111: "TD-CMS-DIGEST-ALGORITHMS", @@ -1381,20 +1388,21 @@ class KRB_PKAuthenticator(ASN1_Packet): ), # [MS-PKCA] sect 2.2.3 ASN1F_optional( - ASN1F_PACKET("paChecksum2", PAChecksum2(), PAChecksum2, explicit_tag=0xA5), + ASN1F_PACKET("paChecksum2", None, PAChecksum2, explicit_tag=0xA5), ), ) - def make_checksum(self, text, h="sha256"): + def make_checksum(self, text, h: str = "sha256"): """ - Populate paChecksum and paChecksum2 + Populate paChecksum """ # paChecksum (always sha-1) self.paChecksum = ASN1_STRING(Hash_SHA().digest(text)) # paChecksum2 - self.paChecksum2 = PAChecksum2() - self.paChecksum2.make(text, h=h) + if h != "sha1": + self.paChecksum2 = PAChecksum2() + self.paChecksum2.make(text, h=h) def verify_checksum(self, text): """ @@ -1403,7 +1411,8 @@ def verify_checksum(self, text): if self.paChecksum.val != Hash_SHA().digest(text): raise ValueError("Bad paChecksum checksum !") - self.paChecksum2.verify(text) + if self.paChecksum2 is not None: + self.paChecksum2.verify(text) # RFC8636 sect 6 @@ -2118,11 +2127,12 @@ def m2i(self, pkt, s): val = super(_KRBERROR_data_Field, self).m2i(pkt, s) if not val[0].val: return val - if pkt.errorCode.val in [14, 24, 25, 36]: + if pkt.errorCode.val in [14, 24, 25, 36, 80]: # 14: KDC_ERR_ETYPE_NOSUPP # 24: KDC_ERR_PREAUTH_FAILED # 25: KDC_ERR_PREAUTH_REQUIRED # 36: KRB_AP_ERR_BADMATCH + # 80: KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED return MethodData(val[0].val, _underlayer=pkt), val[1] elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 32, 41, 60, 62]: # 6: KDC_ERR_C_PRINCIPAL_UNKNOWN @@ -2993,6 +3003,7 @@ class KerberosClient(Automaton): :param mode: the mode to use for the client (default: AS_REQ). :param ip: the IP of the DC (default: discovered by dclocator) :param upn: the UPN of the client. + :param canonicalize: request the UPN to be canonicalized. :param password: the password of the client. :param key: the Key of the client (instead of the password) :param realm: the realm of the domain. (default: from the UPN) @@ -3009,6 +3020,8 @@ class KerberosClient(Automaton): :param armor_ticket_upn: the UPN of the client of the armoring ticket :param armor_ticket_skey: the session Key object of the armoring ticket :param etypes: specify the list of encryption types to support + :param dhashes: specify the list of supported digest algorithms for PKINIT + (defaults to ["sha1", "sha256", "sha384", "sha512"]) AS-REQ only: @@ -3048,12 +3061,14 @@ def __init__( mode=MODE.AS_REQ, ip: Optional[str] = None, upn: Optional[str] = None, + canonicalize: bool = False, password: Optional[str] = None, key: Optional["Key"] = None, realm: Optional[str] = None, x509: Optional[Union[Cert, str]] = None, x509key: Optional[Union[PrivKey, str]] = None, ca: Optional[Union[CertTree, str]] = None, + no_verify_cert: bool = False, p12: Optional[str] = None, spn: Optional[str] = None, ticket: Optional[KRB_Ticket] = None, @@ -3072,6 +3087,7 @@ def __init__( armor_ticket_skey: Optional["Key"] = None, key_list_req: List["EncryptionType"] = [], etypes: Optional[List["EncryptionType"]] = None, + dhashes: Optional[List[str]] = None, pkinit_kex_method: PKINIT_KEX_METHOD = PKINIT_KEX_METHOD.DIFFIE_HELLMAN, port: int = 88, timeout: int = 5, @@ -3081,22 +3097,6 @@ def __init__( import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 from scapy.layers.ldap import dclocator - if not upn: - raise ValueError("Invalid upn") - if not spn: - raise ValueError("Invalid spn") - if realm is None: - if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: - _, realm = _parse_upn(upn) - elif mode == self.MODE.TGS_REQ: - _, realm = _parse_spn(spn) - if not realm and ticket: - # if no realm is specified, but there's a ticket, take the realm - # of the ticket. - realm = ticket.realm.val.decode() - else: - raise ValueError("Invalid realm") - # PKINIT checks if p12 is not None: # password should be None or bytes @@ -3137,12 +3137,58 @@ def __init__( x509key = PrivKey(x509key) if ca and not isinstance(ca, CertList): ca = CertList(ca) + if upn is None and x509: + # For PKINIT, get the UPN from the SAN, if possible and present + if realm is None: + raise ValueError( + "When using PKINIT, you must at least specify the realm= !" + ) + for ext in x509.extensions: + if ext.extnID.val == "2.5.29.17": # subjectAltName + generalName = ext.extnValue.subjectAltName[0].generalName + upn = generalName.value.val.decode("utf-8") + break + if upn is None: + raise ValueError( + "Could not find subjectAltName in certificate !" + " Please provide a UPN." + ) + canonicalize = True + + # UPN, SPN and realm calculation + if not upn: + raise ValueError("Invalid upn") + if realm is None: + if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: + _, realm = _parse_upn(upn) + elif mode == self.MODE.TGS_REQ: + _, realm = _parse_spn(spn) + if not realm and ticket: + # if no realm is specified, but there's a ticket, take the realm + # of the ticket. + realm = ticket.realm.val.decode() + else: + raise ValueError("Invalid realm") + if not spn and mode == self.MODE.AS_REQ and realm: + spn = "krbtgt/" + realm + elif not spn: + raise ValueError("Invalid spn") + # Extra checks for specific requests if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: if not host: raise ValueError("Invalid host") - if x509 is not None and (not x509key or not ca): - raise ValueError("Must provide both 'x509', 'x509key' and 'ca' !") + if x509 is not None: + if x509key and not ca: + if not no_verify_cert: + raise ValueError( + "Using PKINIT without specifying the remote CA is unsafe !" + " Set no_verify_cert=True to bypass this check." + ) + else: + ca = [] + elif not x509key or not ca: + raise ValueError("Must provide both 'x509', 'x509key' and 'ca' !") elif mode == self.MODE.TGS_REQ: if not ticket: raise ValueError("Invalid ticket") @@ -3174,6 +3220,8 @@ def __init__( "Cannot specify armor_ticket without armor_ticket_{upn,skey}" ) + # Provide default supported encryption types. For SALT mode, we discard + # the encryption types that don't have a salt. if mode == self.MODE.GET_SALT: if etypes is not None: raise ValueError("Cannot specify etypes in GET_SALT mode !") @@ -3187,7 +3235,6 @@ def __init__( EncryptionType.AES256_CTS_HMAC_SHA1_96, EncryptionType.AES128_CTS_HMAC_SHA1_96, EncryptionType.RC4_HMAC, - EncryptionType.RC4_HMAC_EXP, EncryptionType.DES_CBC_MD5, ] self.etypes = etypes @@ -3208,6 +3255,7 @@ def __init__( self.password = password and bytes_encode(password) self.spn = spn self.upn = upn + self.canonicalize = canonicalize # Whether we request canonicalization self.realm = realm.upper() self.x509 = x509 self.x509key = x509key @@ -3233,7 +3281,12 @@ def __init__( # This marks that we sent a FAST-req and are awaiting for an answer self.fast_req_sent = False # Session parameters - self.pre_auth = False + if self.x509: + # Windows only assumes it needs a pre-auth when PKINIT is used, + # otherwise it waits to have a PREAUTH_REQUIRED error first. + self.pre_auth = True + else: + self.pre_auth = False self.pa_type = None # preauth-type that's used self.fast_rep = None self.fast_error = None @@ -3241,11 +3294,17 @@ def __init__( self.fast_armorkey = None # The armor key self.fxcookie = None self.pkinit_dh_key = None + self.no_verify_cert = no_verify_cert if ca is not None: self.pkinit_cms = CMS_Engine(ca) else: self.pkinit_cms = None + if dhashes is None: + self.dhashes = ["sha1", "sha256", "sha384", "sha512"] + else: + self.dhashes = dhashes + # Launch the client sock = self._connect() super(KerberosClient, self).__init__( sock=sock, @@ -3455,7 +3514,8 @@ def as_req(self): address=ASN1_STRING(self.host.ljust(16, " ")), ) ] - kdc_req.cname = PrincipalName.fromUPN(self.upn) + kdc_req.addresses = None + kdc_req.cname = PrincipalName.fromUPN(self.upn, canonicalize=self.canonicalize) kdc_req.sname = PrincipalName.fromSPN(self.spn) # 2. Build the list of PADATA @@ -3507,7 +3567,7 @@ def as_req(self): nonce=ASN1_INTEGER(RandNum(0, 0x7FFFFFFF)._fix()), ), clientPublicValue=None, # Used only in DH mode - supportedCMSTypes=None, + supportedCMSTypes=[], clientDHNonce=None, supportedKDFs=None, ) @@ -3515,7 +3575,7 @@ def as_req(self): if self.pkinit_kex_method == PKINIT_KEX_METHOD.DIFFIE_HELLMAN: # RFC4556 - 3.2.3.1. Diffie-Hellman Key Exchange - # We use modp2048 + # We (and Windows) use modp2048 dh_parameters = _ffdh_groups["modp2048"][0] self.pkinit_dh_key = dh_parameters.generate_private_key() numbers = dh_parameters.parameter_numbers() @@ -3530,6 +3590,7 @@ def as_req(self): g=ASN1_INTEGER(numbers.g), # q: see ERRATA 1 of RFC4556 q=ASN1_INTEGER(numbers.q or (numbers.p - 1) // 2), + j=None, ), ), subjectPublicKey=DHPublicKey( @@ -3565,8 +3626,15 @@ def as_req(self): else: raise ValueError - # Populate paChecksum and PAChecksum2 - authpack.pkAuthenticator.make_checksum(bytes(kdc_req)) + # Find a supported digest hash. Windows 25H2 still defaults + # to SHA1 unless a client policy has been applied. + dhash = next(iter(self.dhashes)) + + # Populate paChecksum + authpack.pkAuthenticator.make_checksum( + bytes(kdc_req), + h=dhash, + ) # Sign the AuthPack signedAuthpack = self.pkinit_cms.sign( @@ -3574,6 +3642,7 @@ def as_req(self): ASN1_OID("id-pkinit-authData"), self.x509, self.x509key, + dhash=dhash, ) # Build PA-DATA @@ -3586,6 +3655,14 @@ def as_req(self): kdcPkId=None, ), ) + + # RFC 4557 extension - OCSP + padata.insert( + 0, + PADATA( + padataType=18, # PA-PK-OCSP-RESPONSE + ), + ) else: # Key-based factor @@ -3784,7 +3861,7 @@ def tgs_req(self): _, crealm = _parse_upn(self.upn) authenticator = KRB_Authenticator( crealm=ASN1_GENERAL_STRING(crealm), - cname=PrincipalName.fromUPN(self.upn), + cname=PrincipalName.fromUPN(self.upn, canonicalize=self.canonicalize), cksum=None, ctime=ASN1_GENERALIZED_TIME(now_time), cusec=ASN1_INTEGER(0), @@ -3899,6 +3976,7 @@ def _process_padatas_and_key(self, padatas, etype: "EncryptionType" = None): keyinfo = self.pkinit_cms.verify( padata.padataValue.rep.dhSignedData, eContentType=ASN1_OID("id-pkinit-DHKeyData"), + no_verify_cert=self.no_verify_cert, ) # If 'etype' is None, we're in an error. Since we verified @@ -3925,6 +4003,9 @@ def _process_padatas_and_key(self, padatas, etype: "EncryptionType" = None): else: raise ValueError + elif padata.padataType == 111: # TD-CMS-DIGEST-ALGORITHMS + self.dhashes = [x.algorithm.oidname for x in padata.padataValue.seq] + elif padata.padataType == 133: # PA-FX-COOKIE # Get cookie and store it self.fxcookie = padata.padataValue @@ -4021,7 +4102,7 @@ def receive_krb_error_as_req(self, pkt): return if pkt.root.errorCode == 25: # KDC_ERR_PREAUTH_REQUIRED - if not self.key and not self.x509: + if not self.key: log_runtime.error( "Got 'KDC_ERR_PREAUTH_REQUIRED', " "but no possible key could be computed." @@ -4030,6 +4111,9 @@ def receive_krb_error_as_req(self, pkt): self.should_followup = True self.pre_auth = True raise self.BEGIN() + elif pkt.root.errorCode == 80: # KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED + self.should_followup = True + raise self.BEGIN() else: self._show_krb_error(pkt) raise self.FINAL() @@ -4068,6 +4152,11 @@ def decrypt_as_rep(self, pkt): self.fast_armorkey, bytes(pkt.root.ticket), ) + # Process pa of FAST response + self._process_padatas_and_key( + self.fast_rep.padata, + etype=pkt.root.encPart.etype.val, + ) self.fast_rep = None elif self.fast: raise ValueError("Answer was not FAST ! Is it supported?") @@ -4204,7 +4293,7 @@ def _spn_are_equal(spn1, spn2): def krb_as_req( - upn: str, + upn: Optional[str] = None, spn: Optional[str] = None, ip: Optional[str] = None, key: Optional["Key"] = None, @@ -4248,12 +4337,10 @@ def krb_as_req( ...: f4e99205e78f8da7681d4ec5520ae4815543720c2a647c1ae814c9")) >>> krb_as_req("user1@DOMAIN.LOCAL", ip="192.168.122.17", key=key) - Example using PKINIT with a p12:: + Example using PKINIT with a p12 ("password" is the password of the p12):: - >>> krb_as_req("user1@DOMAIN.LOCAL", p12="./store.p12", password="password") + >>> krb_as_req(p12="./store.p12", realm="DOMAIN.LOCAL", password="password") """ - if realm is None: - _, realm = _parse_upn(upn) if key is None and p12 is None and x509 is None: if password is None: try: @@ -4266,7 +4353,7 @@ def krb_as_req( mode=KerberosClient.MODE.AS_REQ, realm=realm, ip=ip, - spn=spn or "krbtgt/" + realm, + spn=spn, host=host, upn=upn, password=password, diff --git a/scapy/layers/ldap.py b/scapy/layers/ldap.py index 047ece69199..d1acbc7ad06 100644 --- a/scapy/layers/ldap.py +++ b/scapy/layers/ldap.py @@ -61,6 +61,7 @@ from scapy.asn1packet import ASN1_Packet from scapy.config import conf from scapy.compat import StrEnum +from scapy.consts import WINDOWS from scapy.error import log_runtime from scapy.fields import ( FieldLenField, @@ -1718,7 +1719,7 @@ class LDAP_Exception(RuntimeError): def __init__(self, *args, **kwargs): resp = kwargs.pop("resp", None) if resp: - self.resultCode = resp.protocolOp.resultCode + self.resultCode = resp.protocolOp.sprintf("%resultCode%") self.diagnosticMessage = resp.protocolOp.diagnosticMessage.val.rstrip( b"\x00" ).decode(errors="backslashreplace") @@ -2013,7 +2014,17 @@ def bind( from scapy.layers.spnego import SPNEGOSSP if not isinstance(self.ssp, SPNEGOSSP): - raise ValueError("Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !") + if WINDOWS: + from scapy.arch.windows.sspi import WinSSP + + if not isinstance(self.ssp, WinSSP): + raise ValueError( + "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !" + ) + else: + raise ValueError( + "Only SPNEGOSSP is supported with SASL_GSS_SPNEGO !" + ) elif mech == LDAP_BIND_MECHS.SICILY: from scapy.layers.ntlm import NTLMSSP @@ -2136,7 +2147,8 @@ def bind( ) if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: raise RuntimeError( - "%s: GSS_Init_sec_context failed !" % self.mech.name, + "%s: GSS_Init_sec_context failed with %s !" + % (self.mech.name, repr(status)), ) while token: resp = self.sr1( @@ -2154,9 +2166,11 @@ def bind( resp=resp, ) val = resp.protocolOp.serverSaslCredsData - if not val: - status = resp.protocolOp.resultCode - break + if resp.protocolOp.resultCode not in [0, 14]: + raise LDAP_Exception( + "SASL authentication failed !", + resp=resp, + ) self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, input_token=GSSAPI_BLOB(val), @@ -2166,9 +2180,9 @@ def bind( else: status = GSS_S_COMPLETE if status != GSS_S_COMPLETE: - raise LDAP_Exception( - "%s bind failed !" % self.mech.name, - resp=resp, + raise RuntimeError( + "%s: GSS_Init_sec_context failed with %s !" + % (self.mech.name, repr(status)), ) elif self.mech == LDAP_BIND_MECHS.SASL_GSSAPI: # GSSAPI has 2 extra exchanges diff --git a/scapy/layers/msrpce/raw/ms_srvs.py b/scapy/layers/msrpce/raw/ms_srvs.py index dbf1b251cf5..4f4a91b4904 100644 --- a/scapy/layers/msrpce/raw/ms_srvs.py +++ b/scapy/layers/msrpce/raw/ms_srvs.py @@ -3,8 +3,8 @@ # See https://scapy.net/ for more information # Copyright (C) Gabriel Potter -# ms-srvs.idl compiled on 06/07/2025 -# This file is a stripped version ! Use scapy-rpc for the full. +# [ms-srvs] v39.0 (Mon, 16 Sep 2024) + """ RPC definitions for the following interfaces: - srvsvc (v3.0): 4B324FC8-1670-01D3-1278-5A47BF6EE188 @@ -13,200 +13,395 @@ import uuid -from scapy.fields import StrFixedLenField +from scapy.fields import PacketListField, StrFixedLenField from scapy.layers.dcerpc import ( NDRPacket, DceRpcOp, + NDRByteField, + NDRConfFieldListField, NDRConfPacketListField, NDRConfStrLenField, + NDRConfVarPacketListField, NDRConfVarStrNullField, NDRConfVarStrNullFieldUtf16, + NDRContextHandle, NDRFullEmbPointerField, NDRFullPointerField, NDRIntField, NDRPacketField, + NDRShortField, NDRSignedIntField, NDRUnionField, + NDRVarStrNullField, + NDRVarStrNullFieldUtf16, register_dcerpc_interface, ) -class LPSHARE_INFO_0(NDRPacket): +class LPCONNECTION_INFO_0(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("coni0_id", 0)] + + +class CONNECT_INFO_0_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi0_netname", "")) + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPCONNECTION_INFO_0, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class SHARE_INFO_0_CONTAINER(NDRPacket): +class LPCONNECTION_INFO_1(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("coni1_id", 0), + NDRIntField("coni1_type", 0), + NDRIntField("coni1_num_opens", 0), + NDRIntField("coni1_num_users", 0), + NDRIntField("coni1_time", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("coni1_username", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("coni1_netname", "")), + ] + + +class CONNECT_INFO_1_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_0()], - LPSHARE_INFO_0, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPCONNECTION_INFO_1, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_INFO_1(NDRPacket): +class LPCONNECT_ENUM_STRUCT(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi1_netname", "")), - NDRIntField("shi1_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi1_remark", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ + ( + NDRFullEmbPointerField( + NDRPacketField( + "ConnectInfo", + CONNECT_INFO_0_CONTAINER(), + CONNECT_INFO_0_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ConnectInfo", + CONNECT_INFO_1_CONTAINER(), + CONNECT_INFO_1_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), + ), + ), + ], + StrFixedLenField("ConnectInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), ] -class SHARE_INFO_1_CONTAINER(NDRPacket): +class NetrConnectionEnum_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("Qualifier", "")), + NDRPacketField("InfoStruct", LPCONNECT_ENUM_STRUCT(), LPCONNECT_ENUM_STRUCT), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrConnectionEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField("InfoStruct", LPCONNECT_ENUM_STRUCT(), LPCONNECT_ENUM_STRUCT), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class LPFILE_INFO_2(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("fi2_id", 0)] + + +class FILE_INFO_2_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_1()], - LPSHARE_INFO_1, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPFILE_INFO_2, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_INFO_2(NDRPacket): +class LPFILE_INFO_3(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_netname", "")), - NDRIntField("shi2_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_remark", "")), - NDRIntField("shi2_permissions", 0), - NDRIntField("shi2_max_uses", 0), - NDRIntField("shi2_current_uses", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_path", "")), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_passwd", "")), + NDRIntField("fi3_id", 0), + NDRIntField("fi3_permissions", 0), + NDRIntField("fi3_num_locks", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("fi3_pathname", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("fi3_username", "")), ] -class SHARE_INFO_2_CONTAINER(NDRPacket): +class FILE_INFO_3_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_2()], - LPSHARE_INFO_2, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPFILE_INFO_3, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_INFO_501(NDRPacket): +class PFILE_ENUM_STRUCT(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi501_netname", "")), - NDRIntField("shi501_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi501_remark", "")), - NDRIntField("shi501_flags", 0), + NDRIntField("Level", 0), + NDRUnionField( + [ + ( + NDRFullEmbPointerField( + NDRPacketField( + "FileInfo", FILE_INFO_2_CONTAINER(), FILE_INFO_2_CONTAINER + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "FileInfo", FILE_INFO_3_CONTAINER(), FILE_INFO_3_CONTAINER + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 3), + (lambda _, val: val.tag == 3), + ), + ), + ], + StrFixedLenField("FileInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), ] -class SHARE_INFO_501_CONTAINER(NDRPacket): +class NetrFileEnum_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("BasePath", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("UserName", "")), + NDRPacketField("InfoStruct", PFILE_ENUM_STRUCT(), PFILE_ENUM_STRUCT), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrFileEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField("InfoStruct", PFILE_ENUM_STRUCT(), PFILE_ENUM_STRUCT), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class NetrFileGetInfo_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("FileId", 0), + NDRIntField("Level", 0), + ] + + +class NetrFileGetInfo_Response(NDRPacket): + fields_desc = [ + NDRUnionField( + [ + ( + NDRFullPointerField( + NDRPacketField("InfoStruct", LPFILE_INFO_2(), LPFILE_INFO_2) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), + ), + ), + ( + NDRFullPointerField( + NDRPacketField("InfoStruct", LPFILE_INFO_3(), LPFILE_INFO_3) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 3), + (lambda _, val: val.tag == 3), + ), + ), + ], + StrFixedLenField("InfoStruct", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + NDRIntField("status", 0), + ] + + +class NetrFileClose_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("FileId", 0), + ] + + +class NetrFileClose_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class LPSESSION_INFO_0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi0_cname", "")) + ] + + +class SESSION_INFO_0_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_501()], - LPSHARE_INFO_501, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPSESSION_INFO_0, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_INFO_502_I(NDRPacket): +class LPSESSION_INFO_1(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_netname", "")), - NDRIntField("shi502_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_remark", "")), - NDRIntField("shi502_permissions", 0), - NDRIntField("shi502_max_uses", 0), - NDRIntField("shi502_current_uses", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_path", "")), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_passwd", "")), - NDRIntField("shi502_reserved", None, size_of="shi502_security_descriptor"), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi1_cname", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi1_username", "")), + NDRIntField("sesi1_num_opens", 0), + NDRIntField("sesi1_time", 0), + NDRIntField("sesi1_idle_time", 0), + NDRIntField("sesi1_user_flags", 0), + ] + + +class SESSION_INFO_1_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( - NDRConfStrLenField( - "shi502_security_descriptor", - "", - size_is=lambda pkt: pkt.shi502_reserved, + NDRConfPacketListField( + "Buffer", [], LPSESSION_INFO_1, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class SHARE_INFO_502_CONTAINER(NDRPacket): +class LPSESSION_INFO_2(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi2_cname", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi2_username", "")), + NDRIntField("sesi2_num_opens", 0), + NDRIntField("sesi2_time", 0), + NDRIntField("sesi2_idle_time", 0), + NDRIntField("sesi2_user_flags", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi2_cltype_name", "")), + ] + + +class SESSION_INFO_2_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_502_I()], - LPSHARE_INFO_502_I, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPSESSION_INFO_2, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_INFO_503_I(NDRPacket): +class LPSESSION_INFO_10(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_netname", "")), - NDRIntField("shi503_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_remark", "")), - NDRIntField("shi503_permissions", 0), - NDRIntField("shi503_max_uses", 0), - NDRIntField("shi503_current_uses", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_path", "")), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_passwd", "")), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_servername", "")), - NDRIntField("shi503_reserved", None, size_of="shi503_security_descriptor"), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi10_cname", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi10_username", "")), + NDRIntField("sesi10_time", 0), + NDRIntField("sesi10_idle_time", 0), + ] + + +class SESSION_INFO_10_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( - NDRConfStrLenField( - "shi503_security_descriptor", - "", - size_is=lambda pkt: pkt.shi503_reserved, + NDRConfPacketListField( + "Buffer", [], LPSESSION_INFO_10, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class SHARE_INFO_503_CONTAINER(NDRPacket): +class LPSESSION_INFO_502(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi502_cname", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi502_username", "")), + NDRIntField("sesi502_num_opens", 0), + NDRIntField("sesi502_time", 0), + NDRIntField("sesi502_idle_time", 0), + NDRIntField("sesi502_user_flags", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi502_cltype_name", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sesi502_transport", "")), + ] + + +class SESSION_INFO_502_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", None, size_of="Buffer"), NDRFullEmbPointerField( NDRConfPacketListField( - "Buffer", - [LPSHARE_INFO_503_I()], - LPSHARE_INFO_503_I, - size_is=lambda pkt: pkt.EntriesRead, + "Buffer", [], LPSESSION_INFO_502, size_is=lambda pkt: pkt.EntriesRead ) ), ] -class LPSHARE_ENUM_STRUCT(NDRPacket): +class PSESSION_ENUM_STRUCT(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("Level", 0), @@ -215,9 +410,9 @@ class LPSHARE_ENUM_STRUCT(NDRPacket): ( NDRFullEmbPointerField( NDRPacketField( - "ShareInfo", - SHARE_INFO_0_CONTAINER(), - SHARE_INFO_0_CONTAINER, + "SessionInfo", + SESSION_INFO_0_CONTAINER(), + SESSION_INFO_0_CONTAINER, ) ), ( @@ -228,9 +423,9 @@ class LPSHARE_ENUM_STRUCT(NDRPacket): ( NDRFullEmbPointerField( NDRPacketField( - "ShareInfo", - SHARE_INFO_1_CONTAINER(), - SHARE_INFO_1_CONTAINER, + "SessionInfo", + SESSION_INFO_1_CONTAINER(), + SESSION_INFO_1_CONTAINER, ) ), ( @@ -241,9 +436,9 @@ class LPSHARE_ENUM_STRUCT(NDRPacket): ( NDRFullEmbPointerField( NDRPacketField( - "ShareInfo", - SHARE_INFO_2_CONTAINER(), - SHARE_INFO_2_CONTAINER, + "SessionInfo", + SESSION_INFO_2_CONTAINER(), + SESSION_INFO_2_CONTAINER, ) ), ( @@ -254,22 +449,22 @@ class LPSHARE_ENUM_STRUCT(NDRPacket): ( NDRFullEmbPointerField( NDRPacketField( - "ShareInfo", - SHARE_INFO_501_CONTAINER(), - SHARE_INFO_501_CONTAINER, + "SessionInfo", + SESSION_INFO_10_CONTAINER(), + SESSION_INFO_10_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 501), - (lambda _, val: val.tag == 501), + (lambda pkt: getattr(pkt, "Level", None) == 10), + (lambda _, val: val.tag == 10), ), ), ( NDRFullEmbPointerField( NDRPacketField( - "ShareInfo", - SHARE_INFO_502_CONTAINER(), - SHARE_INFO_502_CONTAINER, + "SessionInfo", + SESSION_INFO_502_CONTAINER(), + SESSION_INFO_502_CONTAINER, ) ), ( @@ -277,45 +472,98 @@ class LPSHARE_ENUM_STRUCT(NDRPacket): (lambda _, val: val.tag == 502), ), ), - ( - NDRFullEmbPointerField( - NDRPacketField( - "ShareInfo", - SHARE_INFO_503_CONTAINER(), - SHARE_INFO_503_CONTAINER, - ) - ), - ( - (lambda pkt: getattr(pkt, "Level", None) == 503), - (lambda _, val: val.tag == 503), - ), - ), ], - StrFixedLenField("ShareInfo", "", length=0), + StrFixedLenField("SessionInfo", "", length=0), align=(4, 8), switch_fmt=("L", "L"), ), ] -class NetrShareEnum_Request(NDRPacket): +class NetrSessionEnum_Request(NDRPacket): fields_desc = [ NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), - NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ClientName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("UserName", "")), + NDRPacketField("InfoStruct", PSESSION_ENUM_STRUCT(), PSESSION_ENUM_STRUCT), NDRIntField("PreferedMaximumLength", 0), NDRFullPointerField(NDRIntField("ResumeHandle", 0)), ] -class NetrShareEnum_Response(NDRPacket): +class NetrSessionEnum_Response(NDRPacket): fields_desc = [ - NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRPacketField("InfoStruct", PSESSION_ENUM_STRUCT(), PSESSION_ENUM_STRUCT), NDRIntField("TotalEntries", 0), NDRFullPointerField(NDRIntField("ResumeHandle", 0)), NDRIntField("status", 0), ] +class NetrSessionDel_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ClientName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("UserName", "")), + ] + + +class NetrSessionDel_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class LPSHARE_INFO_0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi0_netname", "")) + ] + + +class LPSHARE_INFO_1(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi1_netname", "")), + NDRIntField("shi1_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi1_remark", "")), + ] + + +class LPSHARE_INFO_2(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_netname", "")), + NDRIntField("shi2_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_remark", "")), + NDRIntField("shi2_permissions", 0), + NDRIntField("shi2_max_uses", 0), + NDRIntField("shi2_current_uses", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_path", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi2_passwd", "")), + ] + + +class LPSHARE_INFO_502_I(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_netname", "")), + NDRIntField("shi502_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_remark", "")), + NDRIntField("shi502_permissions", 0), + NDRIntField("shi502_max_uses", 0), + NDRIntField("shi502_current_uses", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_path", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi502_passwd", "")), + NDRIntField("shi502_reserved", None, size_of="shi502_security_descriptor"), + NDRFullEmbPointerField( + NDRConfStrLenField( + "shi502_security_descriptor", + "", + size_is=lambda pkt: pkt.shi502_reserved, + ) + ), + ] + + class LPSHARE_INFO_1004(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ @@ -347,16 +595,43 @@ class LPSHARE_INFO_1005(NDRPacket): fields_desc = [NDRIntField("shi1005_flags", 0)] -class NetrShareGetInfo_Request(NDRPacket): +class LPSHARE_INFO_501(NDRPacket): + ALIGNMENT = (4, 8) fields_desc = [ - NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), - NDRConfVarStrNullFieldUtf16("NetName", ""), - NDRIntField("Level", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi501_netname", "")), + NDRIntField("shi501_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi501_remark", "")), + NDRIntField("shi501_flags", 0), ] -class NetrShareGetInfo_Response(NDRPacket): +class LPSHARE_INFO_503_I(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_netname", "")), + NDRIntField("shi503_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_remark", "")), + NDRIntField("shi503_permissions", 0), + NDRIntField("shi503_max_uses", 0), + NDRIntField("shi503_current_uses", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_path", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_passwd", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("shi503_servername", "")), + NDRIntField("shi503_reserved", None, size_of="shi503_security_descriptor"), + NDRFullEmbPointerField( + NDRConfStrLenField( + "shi503_security_descriptor", + "", + size_is=lambda pkt: pkt.shi503_reserved, + ) + ), + ] + + +class NetrShareAdd_Request(NDRPacket): fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), NDRUnionField( [ ( @@ -468,426 +743,902 @@ class NetrShareGetInfo_Response(NDRPacket): align=(4, 8), switch_fmt=("L", "L"), ), + NDRFullPointerField(NDRIntField("ParmErr", 0)), + ] + + +class NetrShareAdd_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRIntField("ParmErr", 0)), NDRIntField("status", 0), ] -class LPSERVER_INFO_100(NDRPacket): +class SHARE_INFO_0_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv100_platform_id", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv100_name", "")), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_0, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_101(NDRPacket): +class SHARE_INFO_1_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv101_platform_id", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv101_name", "")), - NDRIntField("sv101_version_major", 0), - NDRIntField("sv101_version_minor", 0), - NDRIntField("sv101_version_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv101_comment", "")), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_1, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_102(NDRPacket): +class SHARE_INFO_2_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv102_platform_id", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_name", "")), - NDRIntField("sv102_version_major", 0), - NDRIntField("sv102_version_minor", 0), - NDRIntField("sv102_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_comment", "")), - NDRIntField("sv102_users", 0), - NDRSignedIntField("sv102_disc", 0), - NDRSignedIntField("sv102_hidden", 0), - NDRIntField("sv102_announce", 0), - NDRIntField("sv102_anndelta", 0), - NDRIntField("sv102_licenses", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_userpath", "")), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_2, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_103(NDRPacket): +class SHARE_INFO_501_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv103_platform_id", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_name", "")), - NDRIntField("sv103_version_major", 0), - NDRIntField("sv103_version_minor", 0), - NDRIntField("sv103_type", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_comment", "")), - NDRIntField("sv103_users", 0), - NDRSignedIntField("sv103_disc", 0), - NDRSignedIntField("sv103_hidden", 0), - NDRIntField("sv103_announce", 0), - NDRIntField("sv103_anndelta", 0), - NDRIntField("sv103_licenses", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_userpath", "")), - NDRIntField("sv103_capabilities", 0), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_501, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_502(NDRPacket): - ALIGNMENT = (4, 4) +class SHARE_INFO_502_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv502_sessopens", 0), - NDRIntField("sv502_sessvcs", 0), - NDRIntField("sv502_opensearch", 0), - NDRIntField("sv502_sizreqbuf", 0), - NDRIntField("sv502_initworkitems", 0), - NDRIntField("sv502_maxworkitems", 0), - NDRIntField("sv502_rawworkitems", 0), - NDRIntField("sv502_irpstacksize", 0), - NDRIntField("sv502_maxrawbuflen", 0), - NDRIntField("sv502_sessusers", 0), - NDRIntField("sv502_sessconns", 0), - NDRIntField("sv502_maxpagedmemoryusage", 0), - NDRIntField("sv502_maxnonpagedmemoryusage", 0), - NDRSignedIntField("sv502_enablesoftcompat", 0), - NDRSignedIntField("sv502_enableforcedlogoff", 0), - NDRSignedIntField("sv502_timesource", 0), - NDRSignedIntField("sv502_acceptdownlevelapis", 0), - NDRSignedIntField("sv502_lmannounce", 0), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_502_I, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_503(NDRPacket): +class SHARE_INFO_503_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv503_sessopens", 0), - NDRIntField("sv503_sessvcs", 0), - NDRIntField("sv503_opensearch", 0), - NDRIntField("sv503_sizreqbuf", 0), - NDRIntField("sv503_initworkitems", 0), - NDRIntField("sv503_maxworkitems", 0), - NDRIntField("sv503_rawworkitems", 0), - NDRIntField("sv503_irpstacksize", 0), - NDRIntField("sv503_maxrawbuflen", 0), - NDRIntField("sv503_sessusers", 0), - NDRIntField("sv503_sessconns", 0), - NDRIntField("sv503_maxpagedmemoryusage", 0), - NDRIntField("sv503_maxnonpagedmemoryusage", 0), - NDRSignedIntField("sv503_enablesoftcompat", 0), - NDRSignedIntField("sv503_enableforcedlogoff", 0), - NDRSignedIntField("sv503_timesource", 0), - NDRSignedIntField("sv503_acceptdownlevelapis", 0), - NDRSignedIntField("sv503_lmannounce", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv503_domain", "")), - NDRIntField("sv503_maxcopyreadlen", 0), - NDRIntField("sv503_maxcopywritelen", 0), - NDRIntField("sv503_minkeepsearch", 0), - NDRIntField("sv503_maxkeepsearch", 0), - NDRIntField("sv503_minkeepcomplsearch", 0), - NDRIntField("sv503_maxkeepcomplsearch", 0), - NDRIntField("sv503_threadcountadd", 0), - NDRIntField("sv503_numblockthreads", 0), - NDRIntField("sv503_scavtimeout", 0), - NDRIntField("sv503_minrcvqueue", 0), - NDRIntField("sv503_minfreeworkitems", 0), - NDRIntField("sv503_xactmemsize", 0), - NDRIntField("sv503_threadpriority", 0), - NDRIntField("sv503_maxmpxct", 0), - NDRIntField("sv503_oplockbreakwait", 0), - NDRIntField("sv503_oplockbreakresponsewait", 0), - NDRSignedIntField("sv503_enableoplocks", 0), - NDRSignedIntField("sv503_enableoplockforceclose", 0), - NDRSignedIntField("sv503_enablefcbopens", 0), - NDRSignedIntField("sv503_enableraw", 0), - NDRSignedIntField("sv503_enablesharednetdrives", 0), - NDRIntField("sv503_minfreeconnections", 0), - NDRIntField("sv503_maxfreeconnections", 0), + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSHARE_INFO_503_I, size_is=lambda pkt: pkt.EntriesRead + ) + ), ] -class LPSERVER_INFO_599(NDRPacket): +class LPSHARE_ENUM_STRUCT(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("sv599_sessopens", 0), - NDRIntField("sv599_sessvcs", 0), - NDRIntField("sv599_opensearch", 0), - NDRIntField("sv599_sizreqbuf", 0), - NDRIntField("sv599_initworkitems", 0), - NDRIntField("sv599_maxworkitems", 0), - NDRIntField("sv599_rawworkitems", 0), - NDRIntField("sv599_irpstacksize", 0), - NDRIntField("sv599_maxrawbuflen", 0), - NDRIntField("sv599_sessusers", 0), - NDRIntField("sv599_sessconns", 0), - NDRIntField("sv599_maxpagedmemoryusage", 0), - NDRIntField("sv599_maxnonpagedmemoryusage", 0), - NDRSignedIntField("sv599_enablesoftcompat", 0), - NDRSignedIntField("sv599_enableforcedlogoff", 0), - NDRSignedIntField("sv599_timesource", 0), - NDRSignedIntField("sv599_acceptdownlevelapis", 0), - NDRSignedIntField("sv599_lmannounce", 0), - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv599_domain", "")), - NDRIntField("sv599_maxcopyreadlen", 0), - NDRIntField("sv599_maxcopywritelen", 0), - NDRIntField("sv599_minkeepsearch", 0), - NDRIntField("sv599_maxkeepsearch", 0), - NDRIntField("sv599_minkeepcomplsearch", 0), - NDRIntField("sv599_maxkeepcomplsearch", 0), - NDRIntField("sv599_threadcountadd", 0), - NDRIntField("sv599_numblockthreads", 0), - NDRIntField("sv599_scavtimeout", 0), - NDRIntField("sv599_minrcvqueue", 0), - NDRIntField("sv599_minfreeworkitems", 0), - NDRIntField("sv599_xactmemsize", 0), - NDRIntField("sv599_threadpriority", 0), - NDRIntField("sv599_maxmpxct", 0), - NDRIntField("sv599_oplockbreakwait", 0), - NDRIntField("sv599_oplockbreakresponsewait", 0), - NDRSignedIntField("sv599_enableoplocks", 0), - NDRSignedIntField("sv599_enableoplockforceclose", 0), - NDRSignedIntField("sv599_enablefcbopens", 0), - NDRSignedIntField("sv599_enableraw", 0), - NDRSignedIntField("sv599_enablesharednetdrives", 0), - NDRIntField("sv599_minfreeconnections", 0), - NDRIntField("sv599_maxfreeconnections", 0), - NDRIntField("sv599_initsesstable", 0), - NDRIntField("sv599_initconntable", 0), - NDRIntField("sv599_initfiletable", 0), - NDRIntField("sv599_initsearchtable", 0), - NDRIntField("sv599_alertschedule", 0), - NDRIntField("sv599_errorthreshold", 0), - NDRIntField("sv599_networkerrorthreshold", 0), - NDRIntField("sv599_diskspacethreshold", 0), - NDRIntField("sv599_reserved", 0), - NDRIntField("sv599_maxlinkdelay", 0), - NDRIntField("sv599_minlinkthroughput", 0), - NDRIntField("sv599_linkinfovalidtime", 0), - NDRIntField("sv599_scavqosinfoupdatetime", 0), - NDRIntField("sv599_maxworkitemidletime", 0), + NDRIntField("Level", 0), + NDRUnionField( + [ + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_0_CONTAINER(), + SHARE_INFO_0_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_1_CONTAINER(), + SHARE_INFO_1_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_2_CONTAINER(), + SHARE_INFO_2_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_501_CONTAINER(), + SHARE_INFO_501_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 501), + (lambda _, val: val.tag == 501), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_502_CONTAINER(), + SHARE_INFO_502_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), + ), + ), + ( + NDRFullEmbPointerField( + NDRPacketField( + "ShareInfo", + SHARE_INFO_503_CONTAINER(), + SHARE_INFO_503_CONTAINER, + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), + ), + ), + ], + StrFixedLenField("ShareInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), ] -class LPSERVER_INFO_1005(NDRPacket): - ALIGNMENT = (4, 8) +class NetrShareEnum_Request(NDRPacket): fields_desc = [ - NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv1005_comment", "")) + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), ] -class LPSERVER_INFO_1107(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1107_users", 0)] - - -class LPSERVER_INFO_1010(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1010_disc", 0)] - - -class LPSERVER_INFO_1016(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1016_hidden", 0)] - - -class LPSERVER_INFO_1017(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1017_announce", 0)] - - -class LPSERVER_INFO_1018(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1018_anndelta", 0)] - - -class LPSERVER_INFO_1501(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1501_sessopens", 0)] - - -class LPSERVER_INFO_1502(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1502_sessvcs", 0)] - - -class LPSERVER_INFO_1503(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1503_opensearch", 0)] - - -class LPSERVER_INFO_1506(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1506_maxworkitems", 0)] - - -class LPSERVER_INFO_1510(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1510_sessusers", 0)] - - -class LPSERVER_INFO_1511(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1511_sessconns", 0)] - - -class LPSERVER_INFO_1512(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1512_maxnonpagedmemoryusage", 0)] - - -class LPSERVER_INFO_1513(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1513_maxpagedmemoryusage", 0)] - - -class LPSERVER_INFO_1514(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1514_enablesoftcompat", 0)] - - -class LPSERVER_INFO_1515(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1515_enableforcedlogoff", 0)] - - -class LPSERVER_INFO_1516(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1516_timesource", 0)] - - -class LPSERVER_INFO_1518(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1518_lmannounce", 0)] - - -class LPSERVER_INFO_1523(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1523_maxkeepsearch", 0)] - - -class LPSERVER_INFO_1528(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1528_scavtimeout", 0)] +class NetrShareEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] -class LPSERVER_INFO_1529(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1529_minrcvqueue", 0)] +class NetrShareGetInfo_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("NetName", ""), + NDRIntField("Level", 0), + ] -class LPSERVER_INFO_1530(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1530_minfreeworkitems", 0)] - +class NetrShareGetInfo_Response(NDRPacket): + fields_desc = [ + NDRUnionField( + [ + ( + NDRFullPointerField( + NDRPacketField("InfoStruct", LPSHARE_INFO_0(), LPSHARE_INFO_0) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), + ), + ), + ( + NDRFullPointerField( + NDRPacketField("InfoStruct", LPSHARE_INFO_1(), LPSHARE_INFO_1) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), + ), + ), + ( + NDRFullPointerField( + NDRPacketField("InfoStruct", LPSHARE_INFO_2(), LPSHARE_INFO_2) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_502_I(), LPSHARE_INFO_502_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_1004(), LPSHARE_INFO_1004 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1004), + (lambda _, val: val.tag == 1004), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_1006(), LPSHARE_INFO_1006 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1006), + (lambda _, val: val.tag == 1006), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_1501_I(), LPSHARE_INFO_1501_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1501), + (lambda _, val: val.tag == 1501), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_1005(), LPSHARE_INFO_1005 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1005), + (lambda _, val: val.tag == 1005), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_501(), LPSHARE_INFO_501 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 501), + (lambda _, val: val.tag == 501), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSHARE_INFO_503_I(), LPSHARE_INFO_503_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), + ), + ), + ], + StrFixedLenField("InfoStruct", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + NDRIntField("status", 0), + ] -class LPSERVER_INFO_1533(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1533_maxmpxct", 0)] +class NetrShareSetInfo_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("NetName", ""), + NDRIntField("Level", 0), + NDRUnionField( + [ + ( + NDRFullPointerField( + NDRPacketField("ShareInfo", LPSHARE_INFO_0(), LPSHARE_INFO_0) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), + ), + ), + ( + NDRFullPointerField( + NDRPacketField("ShareInfo", LPSHARE_INFO_1(), LPSHARE_INFO_1) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), + ), + ), + ( + NDRFullPointerField( + NDRPacketField("ShareInfo", LPSHARE_INFO_2(), LPSHARE_INFO_2) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_502_I(), LPSHARE_INFO_502_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_1004(), LPSHARE_INFO_1004 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1004), + (lambda _, val: val.tag == 1004), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_1006(), LPSHARE_INFO_1006 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1006), + (lambda _, val: val.tag == 1006), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_1501_I(), LPSHARE_INFO_1501_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1501), + (lambda _, val: val.tag == 1501), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_1005(), LPSHARE_INFO_1005 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1005), + (lambda _, val: val.tag == 1005), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_501(), LPSHARE_INFO_501 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 501), + (lambda _, val: val.tag == 501), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ShareInfo", LPSHARE_INFO_503_I(), LPSHARE_INFO_503_I + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), + ), + ), + ], + StrFixedLenField("ShareInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + NDRFullPointerField(NDRIntField("ParmErr", 0)), + ] -class LPSERVER_INFO_1534(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1534_oplockbreakwait", 0)] +class NetrShareSetInfo_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRIntField("ParmErr", 0)), + NDRIntField("status", 0), + ] -class LPSERVER_INFO_1535(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1535_oplockbreakresponsewait", 0)] +class NetrShareDel_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("NetName", ""), + NDRIntField("Reserved", 0), + ] -class LPSERVER_INFO_1536(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1536_enableoplocks", 0)] +class NetrShareDel_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] -class LPSERVER_INFO_1538(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1538_enablefcbopens", 0)] +class NetrShareDelSticky_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("NetName", ""), + NDRIntField("Reserved", 0), + ] -class LPSERVER_INFO_1539(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1539_enableraw", 0)] +class NetrShareDelSticky_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] -class LPSERVER_INFO_1540(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1540_enablesharednetdrives", 0)] +class NetrShareCheck_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("Device", ""), + ] -class LPSERVER_INFO_1541(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1541_minfreeconnections", 0)] +class NetrShareCheck_Response(NDRPacket): + fields_desc = [NDRIntField("Type", 0), NDRIntField("status", 0)] -class LPSERVER_INFO_1542(NDRPacket): - ALIGNMENT = (4, 4) - fields_desc = [NDRSignedIntField("sv1542_maxfreeconnections", 0)] +class LPSERVER_INFO_100(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv100_platform_id", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv100_name", "")), + ] -class LPSERVER_INFO_1543(NDRPacket): + +class LPSERVER_INFO_101(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv101_platform_id", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv101_name", "")), + NDRIntField("sv101_version_major", 0), + NDRIntField("sv101_version_minor", 0), + NDRIntField("sv101_version_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv101_comment", "")), + ] + + +class LPSERVER_INFO_102(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv102_platform_id", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_name", "")), + NDRIntField("sv102_version_major", 0), + NDRIntField("sv102_version_minor", 0), + NDRIntField("sv102_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_comment", "")), + NDRIntField("sv102_users", 0), + NDRSignedIntField("sv102_disc", 0), + NDRSignedIntField("sv102_hidden", 0), + NDRIntField("sv102_announce", 0), + NDRIntField("sv102_anndelta", 0), + NDRIntField("sv102_licenses", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv102_userpath", "")), + ] + + +class LPSERVER_INFO_103(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv103_platform_id", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_name", "")), + NDRIntField("sv103_version_major", 0), + NDRIntField("sv103_version_minor", 0), + NDRIntField("sv103_type", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_comment", "")), + NDRIntField("sv103_users", 0), + NDRSignedIntField("sv103_disc", 0), + NDRSignedIntField("sv103_hidden", 0), + NDRIntField("sv103_announce", 0), + NDRIntField("sv103_anndelta", 0), + NDRIntField("sv103_licenses", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv103_userpath", "")), + NDRIntField("sv103_capabilities", 0), + ] + + +class LPSERVER_INFO_502(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1543_initsesstable", 0)] + fields_desc = [ + NDRIntField("sv502_sessopens", 0), + NDRIntField("sv502_sessvcs", 0), + NDRIntField("sv502_opensearch", 0), + NDRIntField("sv502_sizreqbuf", 0), + NDRIntField("sv502_initworkitems", 0), + NDRIntField("sv502_maxworkitems", 0), + NDRIntField("sv502_rawworkitems", 0), + NDRIntField("sv502_irpstacksize", 0), + NDRIntField("sv502_maxrawbuflen", 0), + NDRIntField("sv502_sessusers", 0), + NDRIntField("sv502_sessconns", 0), + NDRIntField("sv502_maxpagedmemoryusage", 0), + NDRIntField("sv502_maxnonpagedmemoryusage", 0), + NDRSignedIntField("sv502_enablesoftcompat", 0), + NDRSignedIntField("sv502_enableforcedlogoff", 0), + NDRSignedIntField("sv502_timesource", 0), + NDRSignedIntField("sv502_acceptdownlevelapis", 0), + NDRSignedIntField("sv502_lmannounce", 0), + ] -class LPSERVER_INFO_1544(NDRPacket): +class LPSERVER_INFO_503(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv503_sessopens", 0), + NDRIntField("sv503_sessvcs", 0), + NDRIntField("sv503_opensearch", 0), + NDRIntField("sv503_sizreqbuf", 0), + NDRIntField("sv503_initworkitems", 0), + NDRIntField("sv503_maxworkitems", 0), + NDRIntField("sv503_rawworkitems", 0), + NDRIntField("sv503_irpstacksize", 0), + NDRIntField("sv503_maxrawbuflen", 0), + NDRIntField("sv503_sessusers", 0), + NDRIntField("sv503_sessconns", 0), + NDRIntField("sv503_maxpagedmemoryusage", 0), + NDRIntField("sv503_maxnonpagedmemoryusage", 0), + NDRSignedIntField("sv503_enablesoftcompat", 0), + NDRSignedIntField("sv503_enableforcedlogoff", 0), + NDRSignedIntField("sv503_timesource", 0), + NDRSignedIntField("sv503_acceptdownlevelapis", 0), + NDRSignedIntField("sv503_lmannounce", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv503_domain", "")), + NDRIntField("sv503_maxcopyreadlen", 0), + NDRIntField("sv503_maxcopywritelen", 0), + NDRIntField("sv503_minkeepsearch", 0), + NDRIntField("sv503_maxkeepsearch", 0), + NDRIntField("sv503_minkeepcomplsearch", 0), + NDRIntField("sv503_maxkeepcomplsearch", 0), + NDRIntField("sv503_threadcountadd", 0), + NDRIntField("sv503_numblockthreads", 0), + NDRIntField("sv503_scavtimeout", 0), + NDRIntField("sv503_minrcvqueue", 0), + NDRIntField("sv503_minfreeworkitems", 0), + NDRIntField("sv503_xactmemsize", 0), + NDRIntField("sv503_threadpriority", 0), + NDRIntField("sv503_maxmpxct", 0), + NDRIntField("sv503_oplockbreakwait", 0), + NDRIntField("sv503_oplockbreakresponsewait", 0), + NDRSignedIntField("sv503_enableoplocks", 0), + NDRSignedIntField("sv503_enableoplockforceclose", 0), + NDRSignedIntField("sv503_enablefcbopens", 0), + NDRSignedIntField("sv503_enableraw", 0), + NDRSignedIntField("sv503_enablesharednetdrives", 0), + NDRIntField("sv503_minfreeconnections", 0), + NDRIntField("sv503_maxfreeconnections", 0), + ] + + +class LPSERVER_INFO_599(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("sv599_sessopens", 0), + NDRIntField("sv599_sessvcs", 0), + NDRIntField("sv599_opensearch", 0), + NDRIntField("sv599_sizreqbuf", 0), + NDRIntField("sv599_initworkitems", 0), + NDRIntField("sv599_maxworkitems", 0), + NDRIntField("sv599_rawworkitems", 0), + NDRIntField("sv599_irpstacksize", 0), + NDRIntField("sv599_maxrawbuflen", 0), + NDRIntField("sv599_sessusers", 0), + NDRIntField("sv599_sessconns", 0), + NDRIntField("sv599_maxpagedmemoryusage", 0), + NDRIntField("sv599_maxnonpagedmemoryusage", 0), + NDRSignedIntField("sv599_enablesoftcompat", 0), + NDRSignedIntField("sv599_enableforcedlogoff", 0), + NDRSignedIntField("sv599_timesource", 0), + NDRSignedIntField("sv599_acceptdownlevelapis", 0), + NDRSignedIntField("sv599_lmannounce", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv599_domain", "")), + NDRIntField("sv599_maxcopyreadlen", 0), + NDRIntField("sv599_maxcopywritelen", 0), + NDRIntField("sv599_minkeepsearch", 0), + NDRIntField("sv599_maxkeepsearch", 0), + NDRIntField("sv599_minkeepcomplsearch", 0), + NDRIntField("sv599_maxkeepcomplsearch", 0), + NDRIntField("sv599_threadcountadd", 0), + NDRIntField("sv599_numblockthreads", 0), + NDRIntField("sv599_scavtimeout", 0), + NDRIntField("sv599_minrcvqueue", 0), + NDRIntField("sv599_minfreeworkitems", 0), + NDRIntField("sv599_xactmemsize", 0), + NDRIntField("sv599_threadpriority", 0), + NDRIntField("sv599_maxmpxct", 0), + NDRIntField("sv599_oplockbreakwait", 0), + NDRIntField("sv599_oplockbreakresponsewait", 0), + NDRSignedIntField("sv599_enableoplocks", 0), + NDRSignedIntField("sv599_enableoplockforceclose", 0), + NDRSignedIntField("sv599_enablefcbopens", 0), + NDRSignedIntField("sv599_enableraw", 0), + NDRSignedIntField("sv599_enablesharednetdrives", 0), + NDRIntField("sv599_minfreeconnections", 0), + NDRIntField("sv599_maxfreeconnections", 0), + NDRIntField("sv599_initsesstable", 0), + NDRIntField("sv599_initconntable", 0), + NDRIntField("sv599_initfiletable", 0), + NDRIntField("sv599_initsearchtable", 0), + NDRIntField("sv599_alertschedule", 0), + NDRIntField("sv599_errorthreshold", 0), + NDRIntField("sv599_networkerrorthreshold", 0), + NDRIntField("sv599_diskspacethreshold", 0), + NDRIntField("sv599_reserved", 0), + NDRIntField("sv599_maxlinkdelay", 0), + NDRIntField("sv599_minlinkthroughput", 0), + NDRIntField("sv599_linkinfovalidtime", 0), + NDRIntField("sv599_scavqosinfoupdatetime", 0), + NDRIntField("sv599_maxworkitemidletime", 0), + ] + + +class LPSERVER_INFO_1005(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("sv1005_comment", "")) + ] + + +class LPSERVER_INFO_1107(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1544_initconntable", 0)] + fields_desc = [NDRIntField("sv1107_users", 0)] -class LPSERVER_INFO_1545(NDRPacket): +class LPSERVER_INFO_1010(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1545_initfiletable", 0)] + fields_desc = [NDRSignedIntField("sv1010_disc", 0)] -class LPSERVER_INFO_1546(NDRPacket): +class LPSERVER_INFO_1016(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1546_initsearchtable", 0)] + fields_desc = [NDRSignedIntField("sv1016_hidden", 0)] -class LPSERVER_INFO_1547(NDRPacket): +class LPSERVER_INFO_1017(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1547_alertschedule", 0)] + fields_desc = [NDRIntField("sv1017_announce", 0)] -class LPSERVER_INFO_1548(NDRPacket): +class LPSERVER_INFO_1018(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1548_errorthreshold", 0)] + fields_desc = [NDRIntField("sv1018_anndelta", 0)] -class LPSERVER_INFO_1549(NDRPacket): +class LPSERVER_INFO_1501(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1549_networkerrorthreshold", 0)] + fields_desc = [NDRIntField("sv1501_sessopens", 0)] -class LPSERVER_INFO_1550(NDRPacket): +class LPSERVER_INFO_1502(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1550_diskspacethreshold", 0)] + fields_desc = [NDRIntField("sv1502_sessvcs", 0)] -class LPSERVER_INFO_1552(NDRPacket): +class LPSERVER_INFO_1503(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1552_maxlinkdelay", 0)] + fields_desc = [NDRIntField("sv1503_opensearch", 0)] -class LPSERVER_INFO_1553(NDRPacket): +class LPSERVER_INFO_1506(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1553_minlinkthroughput", 0)] + fields_desc = [NDRIntField("sv1506_maxworkitems", 0)] -class LPSERVER_INFO_1554(NDRPacket): +class LPSERVER_INFO_1510(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1554_linkinfovalidtime", 0)] + fields_desc = [NDRIntField("sv1510_sessusers", 0)] -class LPSERVER_INFO_1555(NDRPacket): +class LPSERVER_INFO_1511(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1555_scavqosinfoupdatetime", 0)] + fields_desc = [NDRIntField("sv1511_sessconns", 0)] -class LPSERVER_INFO_1556(NDRPacket): +class LPSERVER_INFO_1512(NDRPacket): ALIGNMENT = (4, 4) - fields_desc = [NDRIntField("sv1556_maxworkitemidletime", 0)] + fields_desc = [NDRIntField("sv1512_maxnonpagedmemoryusage", 0)] -class NetrServerGetInfo_Request(NDRPacket): +class LPSERVER_INFO_1513(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1513_maxpagedmemoryusage", 0)] + + +class LPSERVER_INFO_1514(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1514_enablesoftcompat", 0)] + + +class LPSERVER_INFO_1515(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1515_enableforcedlogoff", 0)] + + +class LPSERVER_INFO_1516(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1516_timesource", 0)] + + +class LPSERVER_INFO_1518(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1518_lmannounce", 0)] + + +class LPSERVER_INFO_1523(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1523_maxkeepsearch", 0)] + + +class LPSERVER_INFO_1528(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1528_scavtimeout", 0)] + + +class LPSERVER_INFO_1529(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1529_minrcvqueue", 0)] + + +class LPSERVER_INFO_1530(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1530_minfreeworkitems", 0)] + + +class LPSERVER_INFO_1533(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1533_maxmpxct", 0)] + + +class LPSERVER_INFO_1534(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1534_oplockbreakwait", 0)] + + +class LPSERVER_INFO_1535(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1535_oplockbreakresponsewait", 0)] + + +class LPSERVER_INFO_1536(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1536_enableoplocks", 0)] + + +class LPSERVER_INFO_1538(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1538_enablefcbopens", 0)] + + +class LPSERVER_INFO_1539(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1539_enableraw", 0)] + + +class LPSERVER_INFO_1540(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1540_enablesharednetdrives", 0)] + + +class LPSERVER_INFO_1541(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1541_minfreeconnections", 0)] + + +class LPSERVER_INFO_1542(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRSignedIntField("sv1542_maxfreeconnections", 0)] + + +class LPSERVER_INFO_1543(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1543_initsesstable", 0)] + + +class LPSERVER_INFO_1544(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1544_initconntable", 0)] + + +class LPSERVER_INFO_1545(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1545_initfiletable", 0)] + + +class LPSERVER_INFO_1546(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1546_initsearchtable", 0)] + + +class LPSERVER_INFO_1547(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1547_alertschedule", 0)] + + +class LPSERVER_INFO_1548(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1548_errorthreshold", 0)] + + +class LPSERVER_INFO_1549(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1549_networkerrorthreshold", 0)] + + +class LPSERVER_INFO_1550(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1550_diskspacethreshold", 0)] + + +class LPSERVER_INFO_1552(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1552_maxlinkdelay", 0)] + + +class LPSERVER_INFO_1553(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1553_minlinkthroughput", 0)] + + +class LPSERVER_INFO_1554(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1554_linkinfovalidtime", 0)] + + +class LPSERVER_INFO_1555(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1555_scavqosinfoupdatetime", 0)] + + +class LPSERVER_INFO_1556(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [NDRIntField("sv1556_maxworkitemidletime", 0)] + + +class NetrServerGetInfo_Request(NDRPacket): fields_desc = [ NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), NDRIntField("Level", 0), @@ -901,573 +1652,2347 @@ class NetrServerGetInfo_Response(NDRPacket): ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_100(), LPSERVER_INFO_100 + "InfoStruct", LPSERVER_INFO_100(), LPSERVER_INFO_100 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 100), + (lambda _, val: val.tag == 100), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_101(), LPSERVER_INFO_101 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 101), + (lambda _, val: val.tag == 101), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_102(), LPSERVER_INFO_102 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 102), + (lambda _, val: val.tag == 102), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_103(), LPSERVER_INFO_103 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 103), + (lambda _, val: val.tag == 103), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_502(), LPSERVER_INFO_502 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_503(), LPSERVER_INFO_503 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_599(), LPSERVER_INFO_599 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 599), + (lambda _, val: val.tag == 599), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1005(), LPSERVER_INFO_1005 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1005), + (lambda _, val: val.tag == 1005), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1107(), LPSERVER_INFO_1107 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1107), + (lambda _, val: val.tag == 1107), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1010(), LPSERVER_INFO_1010 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1010), + (lambda _, val: val.tag == 1010), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1016(), LPSERVER_INFO_1016 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1016), + (lambda _, val: val.tag == 1016), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1017(), LPSERVER_INFO_1017 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1017), + (lambda _, val: val.tag == 1017), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1018(), LPSERVER_INFO_1018 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1018), + (lambda _, val: val.tag == 1018), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1501(), LPSERVER_INFO_1501 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1501), + (lambda _, val: val.tag == 1501), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1502(), LPSERVER_INFO_1502 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1502), + (lambda _, val: val.tag == 1502), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1503(), LPSERVER_INFO_1503 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1503), + (lambda _, val: val.tag == 1503), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1506(), LPSERVER_INFO_1506 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1506), + (lambda _, val: val.tag == 1506), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1510(), LPSERVER_INFO_1510 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1510), + (lambda _, val: val.tag == 1510), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1511(), LPSERVER_INFO_1511 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1511), + (lambda _, val: val.tag == 1511), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1512(), LPSERVER_INFO_1512 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1512), + (lambda _, val: val.tag == 1512), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1513(), LPSERVER_INFO_1513 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1513), + (lambda _, val: val.tag == 1513), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1514(), LPSERVER_INFO_1514 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1514), + (lambda _, val: val.tag == 1514), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1515(), LPSERVER_INFO_1515 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1515), + (lambda _, val: val.tag == 1515), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1516(), LPSERVER_INFO_1516 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1516), + (lambda _, val: val.tag == 1516), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1518(), LPSERVER_INFO_1518 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1518), + (lambda _, val: val.tag == 1518), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1523(), LPSERVER_INFO_1523 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1523), + (lambda _, val: val.tag == 1523), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1528(), LPSERVER_INFO_1528 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1528), + (lambda _, val: val.tag == 1528), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1529(), LPSERVER_INFO_1529 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1529), + (lambda _, val: val.tag == 1529), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1530(), LPSERVER_INFO_1530 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1530), + (lambda _, val: val.tag == 1530), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1533(), LPSERVER_INFO_1533 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1533), + (lambda _, val: val.tag == 1533), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1534(), LPSERVER_INFO_1534 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1534), + (lambda _, val: val.tag == 1534), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1535(), LPSERVER_INFO_1535 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1535), + (lambda _, val: val.tag == 1535), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1536(), LPSERVER_INFO_1536 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1536), + (lambda _, val: val.tag == 1536), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1538(), LPSERVER_INFO_1538 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1538), + (lambda _, val: val.tag == 1538), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1539(), LPSERVER_INFO_1539 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1539), + (lambda _, val: val.tag == 1539), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1540(), LPSERVER_INFO_1540 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1540), + (lambda _, val: val.tag == 1540), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1541(), LPSERVER_INFO_1541 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1541), + (lambda _, val: val.tag == 1541), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1542(), LPSERVER_INFO_1542 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1542), + (lambda _, val: val.tag == 1542), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1543(), LPSERVER_INFO_1543 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1543), + (lambda _, val: val.tag == 1543), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1544(), LPSERVER_INFO_1544 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1544), + (lambda _, val: val.tag == 1544), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1545(), LPSERVER_INFO_1545 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1545), + (lambda _, val: val.tag == 1545), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1546(), LPSERVER_INFO_1546 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1546), + (lambda _, val: val.tag == 1546), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1547(), LPSERVER_INFO_1547 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1547), + (lambda _, val: val.tag == 1547), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1548(), LPSERVER_INFO_1548 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1548), + (lambda _, val: val.tag == 1548), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1549(), LPSERVER_INFO_1549 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1549), + (lambda _, val: val.tag == 1549), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1550(), LPSERVER_INFO_1550 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1550), + (lambda _, val: val.tag == 1550), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1552(), LPSERVER_INFO_1552 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1552), + (lambda _, val: val.tag == 1552), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1553(), LPSERVER_INFO_1553 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1553), + (lambda _, val: val.tag == 1553), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1554(), LPSERVER_INFO_1554 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1554), + (lambda _, val: val.tag == 1554), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1555(), LPSERVER_INFO_1555 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1555), + (lambda _, val: val.tag == 1555), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "InfoStruct", LPSERVER_INFO_1556(), LPSERVER_INFO_1556 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1556), + (lambda _, val: val.tag == 1556), + ), + ), + ], + StrFixedLenField("InfoStruct", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + NDRIntField("status", 0), + ] + + +class NetrServerSetInfo_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_100(), LPSERVER_INFO_100 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 100), + (lambda _, val: val.tag == 100), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_101(), LPSERVER_INFO_101 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 101), + (lambda _, val: val.tag == 101), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_102(), LPSERVER_INFO_102 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 102), + (lambda _, val: val.tag == 102), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_103(), LPSERVER_INFO_103 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 103), + (lambda _, val: val.tag == 103), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_502(), LPSERVER_INFO_502 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_503(), LPSERVER_INFO_503 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_599(), LPSERVER_INFO_599 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 599), + (lambda _, val: val.tag == 599), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1005(), LPSERVER_INFO_1005 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1005), + (lambda _, val: val.tag == 1005), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1107(), LPSERVER_INFO_1107 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1107), + (lambda _, val: val.tag == 1107), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1010(), LPSERVER_INFO_1010 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1010), + (lambda _, val: val.tag == 1010), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1016(), LPSERVER_INFO_1016 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 100), - (lambda _, val: val.tag == 100), + (lambda pkt: getattr(pkt, "Level", None) == 1016), + (lambda _, val: val.tag == 1016), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_101(), LPSERVER_INFO_101 + "ServerInfo", LPSERVER_INFO_1017(), LPSERVER_INFO_1017 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 101), - (lambda _, val: val.tag == 101), + (lambda pkt: getattr(pkt, "Level", None) == 1017), + (lambda _, val: val.tag == 1017), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_102(), LPSERVER_INFO_102 + "ServerInfo", LPSERVER_INFO_1018(), LPSERVER_INFO_1018 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 102), - (lambda _, val: val.tag == 102), + (lambda pkt: getattr(pkt, "Level", None) == 1018), + (lambda _, val: val.tag == 1018), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_103(), LPSERVER_INFO_103 + "ServerInfo", LPSERVER_INFO_1501(), LPSERVER_INFO_1501 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 103), - (lambda _, val: val.tag == 103), + (lambda pkt: getattr(pkt, "Level", None) == 1501), + (lambda _, val: val.tag == 1501), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_502(), LPSERVER_INFO_502 + "ServerInfo", LPSERVER_INFO_1502(), LPSERVER_INFO_1502 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 502), - (lambda _, val: val.tag == 502), + (lambda pkt: getattr(pkt, "Level", None) == 1502), + (lambda _, val: val.tag == 1502), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_503(), LPSERVER_INFO_503 + "ServerInfo", LPSERVER_INFO_1503(), LPSERVER_INFO_1503 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 503), - (lambda _, val: val.tag == 503), + (lambda pkt: getattr(pkt, "Level", None) == 1503), + (lambda _, val: val.tag == 1503), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_599(), LPSERVER_INFO_599 + "ServerInfo", LPSERVER_INFO_1506(), LPSERVER_INFO_1506 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 599), - (lambda _, val: val.tag == 599), + (lambda pkt: getattr(pkt, "Level", None) == 1506), + (lambda _, val: val.tag == 1506), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1005(), LPSERVER_INFO_1005 + "ServerInfo", LPSERVER_INFO_1510(), LPSERVER_INFO_1510 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1005), - (lambda _, val: val.tag == 1005), + (lambda pkt: getattr(pkt, "Level", None) == 1510), + (lambda _, val: val.tag == 1510), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1107(), LPSERVER_INFO_1107 + "ServerInfo", LPSERVER_INFO_1511(), LPSERVER_INFO_1511 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1107), - (lambda _, val: val.tag == 1107), + (lambda pkt: getattr(pkt, "Level", None) == 1511), + (lambda _, val: val.tag == 1511), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1010(), LPSERVER_INFO_1010 + "ServerInfo", LPSERVER_INFO_1512(), LPSERVER_INFO_1512 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1010), - (lambda _, val: val.tag == 1010), + (lambda pkt: getattr(pkt, "Level", None) == 1512), + (lambda _, val: val.tag == 1512), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1016(), LPSERVER_INFO_1016 + "ServerInfo", LPSERVER_INFO_1513(), LPSERVER_INFO_1513 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1016), - (lambda _, val: val.tag == 1016), + (lambda pkt: getattr(pkt, "Level", None) == 1513), + (lambda _, val: val.tag == 1513), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1017(), LPSERVER_INFO_1017 + "ServerInfo", LPSERVER_INFO_1514(), LPSERVER_INFO_1514 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1017), - (lambda _, val: val.tag == 1017), + (lambda pkt: getattr(pkt, "Level", None) == 1514), + (lambda _, val: val.tag == 1514), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1018(), LPSERVER_INFO_1018 + "ServerInfo", LPSERVER_INFO_1515(), LPSERVER_INFO_1515 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1018), - (lambda _, val: val.tag == 1018), + (lambda pkt: getattr(pkt, "Level", None) == 1515), + (lambda _, val: val.tag == 1515), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1501(), LPSERVER_INFO_1501 + "ServerInfo", LPSERVER_INFO_1516(), LPSERVER_INFO_1516 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1501), - (lambda _, val: val.tag == 1501), + (lambda pkt: getattr(pkt, "Level", None) == 1516), + (lambda _, val: val.tag == 1516), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1502(), LPSERVER_INFO_1502 + "ServerInfo", LPSERVER_INFO_1518(), LPSERVER_INFO_1518 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1502), - (lambda _, val: val.tag == 1502), + (lambda pkt: getattr(pkt, "Level", None) == 1518), + (lambda _, val: val.tag == 1518), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1503(), LPSERVER_INFO_1503 + "ServerInfo", LPSERVER_INFO_1523(), LPSERVER_INFO_1523 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1503), - (lambda _, val: val.tag == 1503), + (lambda pkt: getattr(pkt, "Level", None) == 1523), + (lambda _, val: val.tag == 1523), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1506(), LPSERVER_INFO_1506 + "ServerInfo", LPSERVER_INFO_1528(), LPSERVER_INFO_1528 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1506), - (lambda _, val: val.tag == 1506), + (lambda pkt: getattr(pkt, "Level", None) == 1528), + (lambda _, val: val.tag == 1528), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1510(), LPSERVER_INFO_1510 + "ServerInfo", LPSERVER_INFO_1529(), LPSERVER_INFO_1529 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1510), - (lambda _, val: val.tag == 1510), + (lambda pkt: getattr(pkt, "Level", None) == 1529), + (lambda _, val: val.tag == 1529), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1511(), LPSERVER_INFO_1511 + "ServerInfo", LPSERVER_INFO_1530(), LPSERVER_INFO_1530 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1511), - (lambda _, val: val.tag == 1511), + (lambda pkt: getattr(pkt, "Level", None) == 1530), + (lambda _, val: val.tag == 1530), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1512(), LPSERVER_INFO_1512 + "ServerInfo", LPSERVER_INFO_1533(), LPSERVER_INFO_1533 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1512), - (lambda _, val: val.tag == 1512), + (lambda pkt: getattr(pkt, "Level", None) == 1533), + (lambda _, val: val.tag == 1533), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1513(), LPSERVER_INFO_1513 + "ServerInfo", LPSERVER_INFO_1534(), LPSERVER_INFO_1534 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1513), - (lambda _, val: val.tag == 1513), + (lambda pkt: getattr(pkt, "Level", None) == 1534), + (lambda _, val: val.tag == 1534), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1535(), LPSERVER_INFO_1535 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1535), + (lambda _, val: val.tag == 1535), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1536(), LPSERVER_INFO_1536 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1536), + (lambda _, val: val.tag == 1536), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1538(), LPSERVER_INFO_1538 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1538), + (lambda _, val: val.tag == 1538), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1539(), LPSERVER_INFO_1539 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1539), + (lambda _, val: val.tag == 1539), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1540(), LPSERVER_INFO_1540 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1540), + (lambda _, val: val.tag == 1540), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1541(), LPSERVER_INFO_1541 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1541), + (lambda _, val: val.tag == 1541), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1542(), LPSERVER_INFO_1542 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1542), + (lambda _, val: val.tag == 1542), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1543(), LPSERVER_INFO_1543 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1543), + (lambda _, val: val.tag == 1543), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1544(), LPSERVER_INFO_1544 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1544), + (lambda _, val: val.tag == 1544), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1545(), LPSERVER_INFO_1545 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1545), + (lambda _, val: val.tag == 1545), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1546(), LPSERVER_INFO_1546 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1546), + (lambda _, val: val.tag == 1546), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1547(), LPSERVER_INFO_1547 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1547), + (lambda _, val: val.tag == 1547), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1548(), LPSERVER_INFO_1548 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1548), + (lambda _, val: val.tag == 1548), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1549(), LPSERVER_INFO_1549 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1549), + (lambda _, val: val.tag == 1549), + ), + ), + ( + NDRFullPointerField( + NDRPacketField( + "ServerInfo", LPSERVER_INFO_1550(), LPSERVER_INFO_1550 + ) + ), + ( + (lambda pkt: getattr(pkt, "Level", None) == 1550), + (lambda _, val: val.tag == 1550), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1514(), LPSERVER_INFO_1514 + "ServerInfo", LPSERVER_INFO_1552(), LPSERVER_INFO_1552 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1514), - (lambda _, val: val.tag == 1514), + (lambda pkt: getattr(pkt, "Level", None) == 1552), + (lambda _, val: val.tag == 1552), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1515(), LPSERVER_INFO_1515 + "ServerInfo", LPSERVER_INFO_1553(), LPSERVER_INFO_1553 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1515), - (lambda _, val: val.tag == 1515), + (lambda pkt: getattr(pkt, "Level", None) == 1553), + (lambda _, val: val.tag == 1553), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1516(), LPSERVER_INFO_1516 + "ServerInfo", LPSERVER_INFO_1554(), LPSERVER_INFO_1554 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1516), - (lambda _, val: val.tag == 1516), + (lambda pkt: getattr(pkt, "Level", None) == 1554), + (lambda _, val: val.tag == 1554), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1518(), LPSERVER_INFO_1518 + "ServerInfo", LPSERVER_INFO_1555(), LPSERVER_INFO_1555 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1518), - (lambda _, val: val.tag == 1518), + (lambda pkt: getattr(pkt, "Level", None) == 1555), + (lambda _, val: val.tag == 1555), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1523(), LPSERVER_INFO_1523 + "ServerInfo", LPSERVER_INFO_1556(), LPSERVER_INFO_1556 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1523), - (lambda _, val: val.tag == 1523), + (lambda pkt: getattr(pkt, "Level", None) == 1556), + (lambda _, val: val.tag == 1556), ), ), + ], + StrFixedLenField("ServerInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + NDRFullPointerField(NDRIntField("ParmErr", 0)), + ] + + +class NetrServerSetInfo_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRIntField("ParmErr", 0)), + NDRIntField("status", 0), + ] + + +class LPDISK_INFO(NDRPacket): + ALIGNMENT = (2, 2) + fields_desc = [NDRVarStrNullFieldUtf16("Disk", "")] + + +class DISK_ENUM_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfVarPacketListField( + "Buffer", + [], + LPDISK_INFO, + size_is=lambda pkt: pkt.EntriesRead, + length_is=lambda pkt: pkt.EntriesRead, + ) + ), + ] + + +class NetrServerDiskEnum_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRPacketField("DiskInfoStruct", DISK_ENUM_CONTAINER(), DISK_ENUM_CONTAINER), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrServerDiskEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField("DiskInfoStruct", DISK_ENUM_CONTAINER(), DISK_ENUM_CONTAINER), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class LPSTAT_SERVER_0(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [ + NDRIntField("sts0_start", 0), + NDRIntField("sts0_fopens", 0), + NDRIntField("sts0_devopens", 0), + NDRIntField("sts0_jobsqueued", 0), + NDRIntField("sts0_sopens", 0), + NDRIntField("sts0_stimedout", 0), + NDRIntField("sts0_serrorout", 0), + NDRIntField("sts0_pwerrors", 0), + NDRIntField("sts0_permerrors", 0), + NDRIntField("sts0_syserrors", 0), + NDRIntField("sts0_bytessent_low", 0), + NDRIntField("sts0_bytessent_high", 0), + NDRIntField("sts0_bytesrcvd_low", 0), + NDRIntField("sts0_bytesrcvd_high", 0), + NDRIntField("sts0_avresponse", 0), + NDRIntField("sts0_reqbufneed", 0), + NDRIntField("sts0_bigbufneed", 0), + ] + + +class NetrServerStatisticsGet_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("Service", "")), + NDRIntField("Level", 0), + NDRIntField("Options", 0), + ] + + +class NetrServerStatisticsGet_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField( + NDRPacketField("InfoStruct", LPSTAT_SERVER_0(), LPSTAT_SERVER_0) + ), + NDRIntField("status", 0), + ] + + +class LPSERVER_TRANSPORT_INFO_0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti0_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti0_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti0_transportaddress", + "", + size_is=lambda pkt: pkt.svti0_transportaddresslength, + ) + ), + NDRIntField( + "svti0_transportaddresslength", None, size_of="svti0_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti0_networkaddress", "")), + ] + + +class NetrServerTransportAdd_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRPacketField( + "Buffer", LPSERVER_TRANSPORT_INFO_0(), LPSERVER_TRANSPORT_INFO_0 + ), + ] + + +class NetrServerTransportAdd_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class PSERVER_XPORT_INFO_0_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", + [], + LPSERVER_TRANSPORT_INFO_0, + size_is=lambda pkt: pkt.EntriesRead, + ) + ), + ] + + +class LPSERVER_TRANSPORT_INFO_1(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti1_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti1_transportaddress", + "", + size_is=lambda pkt: pkt.svti1_transportaddresslength, + ) + ), + NDRIntField( + "svti1_transportaddresslength", None, size_of="svti1_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_domain", "")), + ] + + +class PSERVER_XPORT_INFO_1_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", + [], + LPSERVER_TRANSPORT_INFO_1, + size_is=lambda pkt: pkt.EntriesRead, + ) + ), + ] + + +class LPSERVER_TRANSPORT_INFO_2(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti2_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti2_transportaddress", + "", + size_is=lambda pkt: pkt.svti2_transportaddresslength, + ) + ), + NDRIntField( + "svti2_transportaddresslength", None, size_of="svti2_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_domain", "")), + NDRIntField("svti2_flags", 0), + ] + + +class PSERVER_XPORT_INFO_2_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", + [], + LPSERVER_TRANSPORT_INFO_2, + size_is=lambda pkt: pkt.EntriesRead, + ) + ), + ] + + +class LPSERVER_TRANSPORT_INFO_3(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti3_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti3_transportaddress", + "", + size_is=lambda pkt: pkt.svti3_transportaddresslength, + ) + ), + NDRIntField( + "svti3_transportaddresslength", None, size_of="svti3_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_domain", "")), + NDRIntField("svti3_flags", 0), + NDRIntField("svti3_passwordlength", 0), + StrFixedLenField("svti3_password", "", length=256), + ] + + +class PSERVER_XPORT_INFO_3_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", + [], + LPSERVER_TRANSPORT_INFO_3, + size_is=lambda pkt: pkt.EntriesRead, + ) + ), + ] + + +class LPSERVER_XPORT_ENUM_STRUCT(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("Level", 0), + NDRUnionField( + [ ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1528(), LPSERVER_INFO_1528 + "XportInfo", + PSERVER_XPORT_INFO_0_CONTAINER(), + PSERVER_XPORT_INFO_0_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1528), - (lambda _, val: val.tag == 1528), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), ), ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1529(), LPSERVER_INFO_1529 + "XportInfo", + PSERVER_XPORT_INFO_1_CONTAINER(), + PSERVER_XPORT_INFO_1_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1529), - (lambda _, val: val.tag == 1529), + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), ), ), ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1530(), LPSERVER_INFO_1530 + "XportInfo", + PSERVER_XPORT_INFO_2_CONTAINER(), + PSERVER_XPORT_INFO_2_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1530), - (lambda _, val: val.tag == 1530), + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), ), ), ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1533(), LPSERVER_INFO_1533 + "XportInfo", + PSERVER_XPORT_INFO_3_CONTAINER(), + PSERVER_XPORT_INFO_3_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1533), - (lambda _, val: val.tag == 1533), + (lambda pkt: getattr(pkt, "Level", None) == 3), + (lambda _, val: val.tag == 3), ), ), + ], + StrFixedLenField("XportInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerTransportEnum_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField( + "InfoStruct", LPSERVER_XPORT_ENUM_STRUCT(), LPSERVER_XPORT_ENUM_STRUCT + ), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrServerTransportEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField( + "InfoStruct", LPSERVER_XPORT_ENUM_STRUCT(), LPSERVER_XPORT_ENUM_STRUCT + ), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class NetrServerTransportDel_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRPacketField( + "Buffer", LPSERVER_TRANSPORT_INFO_0(), LPSERVER_TRANSPORT_INFO_0 + ), + ] + + +class NetrServerTransportDel_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class LPTIME_OF_DAY_INFO(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [ + NDRIntField("tod_elapsedt", 0), + NDRIntField("tod_msecs", 0), + NDRIntField("tod_hours", 0), + NDRIntField("tod_mins", 0), + NDRIntField("tod_secs", 0), + NDRIntField("tod_hunds", 0), + NDRSignedIntField("tod_timezone", 0), + NDRIntField("tod_tinterval", 0), + NDRIntField("tod_day", 0), + NDRIntField("tod_month", 0), + NDRIntField("tod_year", 0), + NDRIntField("tod_weekday", 0), + ] + + +class NetrRemoteTOD_Request(NDRPacket): + fields_desc = [NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", ""))] + + +class NetrRemoteTOD_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField( + NDRPacketField("BufferPtr", LPTIME_OF_DAY_INFO(), LPTIME_OF_DAY_INFO) + ), + NDRIntField("status", 0), + ] + + +class NetprPathType_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("PathName", ""), + NDRIntField("Flags", 0), + ] + + +class NetprPathType_Response(NDRPacket): + fields_desc = [NDRIntField("PathType", 0), NDRIntField("status", 0)] + + +class NetprPathCanonicalize_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("PathName", ""), + NDRIntField("OutbufLen", 0), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + NDRIntField("PathType", 0), + NDRIntField("Flags", 0), + ] + + +class NetprPathCanonicalize_Response(NDRPacket): + fields_desc = [ + NDRConfStrLenField("Outbuf", "", size_is=lambda pkt: pkt.OutbufLen), + NDRIntField("PathType", 0), + NDRIntField("status", 0), + ] + + +class NetprPathCompare_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("PathName1", ""), + NDRConfVarStrNullFieldUtf16("PathName2", ""), + NDRIntField("PathType", 0), + NDRIntField("Flags", 0), + ] + + +class NetprPathCompare_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetprNameValidate_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("Name", ""), + NDRIntField("NameType", 0), + NDRIntField("Flags", 0), + ] + + +class NetprNameValidate_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetprNameCanonicalize_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("Name", ""), + NDRIntField("OutbufLen", 0), + NDRIntField("NameType", 0), + NDRIntField("Flags", 0), + ] + + +class NetprNameCanonicalize_Response(NDRPacket): + fields_desc = [ + NDRConfFieldListField( + "Outbuf", [], NDRShortField("", 0), size_is=lambda pkt: pkt.OutbufLen + ), + NDRIntField("status", 0), + ] + + +class NetprNameCompare_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("Name1", ""), + NDRConfVarStrNullFieldUtf16("Name2", ""), + NDRIntField("NameType", 0), + NDRIntField("Flags", 0), + ] + + +class NetprNameCompare_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrShareEnumSticky_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrShareEnumSticky_Response(NDRPacket): + fields_desc = [ + NDRPacketField("InfoStruct", LPSHARE_ENUM_STRUCT(), LPSHARE_ENUM_STRUCT), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class NetrShareDelStart_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("NetName", ""), + NDRIntField("Reserved", 0), + ] + + +class NetrShareDelStart_Response(NDRPacket): + fields_desc = [ + NDRPacketField("ContextHandle", NDRContextHandle(), NDRContextHandle), + NDRIntField("status", 0), + ] + + +class NetrShareDelCommit_Request(NDRPacket): + fields_desc = [ + NDRPacketField("ContextHandle", NDRContextHandle(), NDRContextHandle) + ] + + +class NetrShareDelCommit_Response(NDRPacket): + fields_desc = [ + NDRPacketField("ContextHandle", NDRContextHandle(), NDRContextHandle), + NDRIntField("status", 0), + ] + + +class PADT_SECURITY_DESCRIPTOR(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("Length", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfStrLenField("Buffer", "", size_is=lambda pkt: pkt.Length) + ), + ] + + +class NetrpGetFileSecurity_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ShareName", "")), + NDRConfVarStrNullFieldUtf16("lpFileName", ""), + NDRIntField("RequestedInformation", 0), + ] + + +class NetrpGetFileSecurity_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField( + NDRPacketField( + "SecurityDescriptor", + PADT_SECURITY_DESCRIPTOR(), + PADT_SECURITY_DESCRIPTOR, + ) + ), + NDRIntField("status", 0), + ] + + +class NetrpSetFileSecurity_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ShareName", "")), + NDRConfVarStrNullFieldUtf16("lpFileName", ""), + NDRIntField("SecurityInformation", 0), + NDRPacketField( + "SecurityDescriptor", PADT_SECURITY_DESCRIPTOR(), PADT_SECURITY_DESCRIPTOR + ), + ] + + +class NetrpSetFileSecurity_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class SERVER_TRANSPORT_INFO_0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti0_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti0_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti0_transportaddress", + "", + size_is=lambda pkt: pkt.svti0_transportaddresslength, + ) + ), + NDRIntField( + "svti0_transportaddresslength", None, size_of="svti0_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti0_networkaddress", "")), + ] + + +class SERVER_TRANSPORT_INFO_1(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti1_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti1_transportaddress", + "", + size_is=lambda pkt: pkt.svti1_transportaddresslength, + ) + ), + NDRIntField( + "svti1_transportaddresslength", None, size_of="svti1_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti1_domain", "")), + ] + + +class SERVER_TRANSPORT_INFO_2(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti2_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti2_transportaddress", + "", + size_is=lambda pkt: pkt.svti2_transportaddresslength, + ) + ), + NDRIntField( + "svti2_transportaddresslength", None, size_of="svti2_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti2_domain", "")), + NDRIntField("svti2_flags", 0), + ] + + +class SERVER_TRANSPORT_INFO_3(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("svti3_numberofvcs", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_transportname", "")), + NDRFullEmbPointerField( + NDRConfStrLenField( + "svti3_transportaddress", + "", + size_is=lambda pkt: pkt.svti3_transportaddresslength, + ) + ), + NDRIntField( + "svti3_transportaddresslength", None, size_of="svti3_transportaddress" + ), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_networkaddress", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("svti3_domain", "")), + NDRIntField("svti3_flags", 0), + NDRIntField("svti3_passwordlength", 0), + StrFixedLenField("svti3_password", "", length=256), + ] + + +class NetrServerTransportAddEx_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1534(), LPSERVER_INFO_1534 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_0(), SERVER_TRANSPORT_INFO_0 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1534), - (lambda _, val: val.tag == 1534), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1535(), LPSERVER_INFO_1535 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_1(), SERVER_TRANSPORT_INFO_1 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1535), - (lambda _, val: val.tag == 1535), + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1536(), LPSERVER_INFO_1536 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_2(), SERVER_TRANSPORT_INFO_2 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1536), - (lambda _, val: val.tag == 1536), + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1538(), LPSERVER_INFO_1538 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_3(), SERVER_TRANSPORT_INFO_3 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1538), - (lambda _, val: val.tag == 1538), + (lambda pkt: getattr(pkt, "Level", None) == 3), + (lambda _, val: val.tag == 3), ), ), + ], + StrFixedLenField("Buffer", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerTransportAddEx_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsGetVersion_Request(NDRPacket): + fields_desc = [NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", ""))] + + +class NetrDfsGetVersion_Response(NDRPacket): + fields_desc = [NDRIntField("Version", 0), NDRIntField("status", 0)] + + +class GUID(NDRPacket): + ALIGNMENT = (4, 4) + fields_desc = [ + NDRIntField("Data1", 0), + NDRShortField("Data2", 0), + NDRShortField("Data3", 0), + StrFixedLenField("Data4", "", length=8), + ] + + +class LPNET_DFS_ENTRY_ID(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRPacketField("Uid", GUID(), GUID), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("Prefix", "")), + ] + + +class LPNET_DFS_ENTRY_ID_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("Count", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPNET_DFS_ENTRY_ID, size_is=lambda pkt: pkt.Count + ) + ), + ] + + +class NetrDfsCreateLocalPartition_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("ShareName", ""), + NDRPacketField("EntryUid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("EntryPrefix", ""), + NDRConfVarStrNullFieldUtf16("ShortName", ""), + NDRPacketField( + "RelationInfo", LPNET_DFS_ENTRY_ID_CONTAINER(), LPNET_DFS_ENTRY_ID_CONTAINER + ), + NDRSignedIntField("Force", 0), + ] + + +class NetrDfsCreateLocalPartition_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsDeleteLocalPartition_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("Uid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + ] + + +class NetrDfsDeleteLocalPartition_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsSetLocalVolumeState_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("Uid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + NDRIntField("State", 0), + ] + + +class NetrDfsSetLocalVolumeState_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsCreateExitPoint_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("Uid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + NDRIntField("Type", 0), + NDRIntField("ShortPrefixLen", 0), + ] + + +class NetrDfsCreateExitPoint_Response(NDRPacket): + fields_desc = [ + NDRConfFieldListField( + "ShortPrefix", + [], + NDRShortField("", 0), + size_is=lambda pkt: pkt.ShortPrefixLen, + ), + NDRIntField("status", 0), + ] + + +class NetrDfsDeleteExitPoint_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("Uid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + NDRIntField("Type", 0), + ] + + +class NetrDfsDeleteExitPoint_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsModifyPrefix_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField("Uid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("Prefix", ""), + ] + + +class NetrDfsModifyPrefix_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrDfsFixLocalVolume_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRConfVarStrNullFieldUtf16("VolumeName", ""), + NDRIntField("EntryType", 0), + NDRIntField("ServiceType", 0), + NDRConfVarStrNullFieldUtf16("StgId", ""), + NDRPacketField("EntryUid", GUID(), GUID), + NDRConfVarStrNullFieldUtf16("EntryPrefix", ""), + NDRPacketField( + "RelationInfo", LPNET_DFS_ENTRY_ID_CONTAINER(), LPNET_DFS_ENTRY_ID_CONTAINER + ), + NDRIntField("CreateDisposition", 0), + ] + + +class NetrDfsFixLocalVolume_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class DFS_SITENAME_INFO(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("SiteFlags", 0), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("SiteName", "")), + ] + + +class LPDFS_SITELIST_INFO(NDRPacket): + ALIGNMENT = (4, 4) + DEPORTED_CONFORMANTS = ["Site"] + fields_desc = [ + NDRIntField("cSites", None, size_of="Site"), + NDRConfPacketListField( + "Site", + [], + DFS_SITENAME_INFO, + size_is=lambda pkt: pkt.cSites, + conformant_in_struct=True, + ), + ] + + +class NetrDfsManagerReportSiteInfo_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRFullPointerField( + NDRFullPointerField( + NDRPacketField("ppSiteInfo", LPDFS_SITELIST_INFO(), LPDFS_SITELIST_INFO) + ) + ), + ] + + +class NetrDfsManagerReportSiteInfo_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField( + NDRFullPointerField( + NDRPacketField("ppSiteInfo", LPDFS_SITELIST_INFO(), LPDFS_SITELIST_INFO) + ) + ), + NDRIntField("status", 0), + ] + + +class NetrServerTransportDelEx_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1539(), LPSERVER_INFO_1539 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_0(), SERVER_TRANSPORT_INFO_0 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1539), - (lambda _, val: val.tag == 1539), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1540(), LPSERVER_INFO_1540 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_1(), SERVER_TRANSPORT_INFO_1 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1540), - (lambda _, val: val.tag == 1540), + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1541(), LPSERVER_INFO_1541 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_2(), SERVER_TRANSPORT_INFO_2 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1541), - (lambda _, val: val.tag == 1541), + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), ), ), ( - NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1542(), LPSERVER_INFO_1542 - ) + NDRPacketField( + "Buffer", SERVER_TRANSPORT_INFO_3(), SERVER_TRANSPORT_INFO_3 ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1542), - (lambda _, val: val.tag == 1542), + (lambda pkt: getattr(pkt, "Level", None) == 3), + (lambda _, val: val.tag == 3), ), ), + ], + StrFixedLenField("Buffer", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerTransportDelEx_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class LPSERVER_ALIAS_INFO_0(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("srvai0_alias", "")), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("srvai0_target", "")), + NDRByteField("srvai0_default", 0), + NDRIntField("srvai0_reserved", 0), + ] + + +class NetrServerAliasAdd_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1543(), LPSERVER_INFO_1543 + "InfoStruct", LPSERVER_ALIAS_INFO_0(), LPSERVER_ALIAS_INFO_0 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1543), - (lambda _, val: val.tag == 1543), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), - ), + ) + ], + StrFixedLenField("InfoStruct", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerAliasAdd_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class SERVER_ALIAS_INFO_0_CONTAINER(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("EntriesRead", None, size_of="Buffer"), + NDRFullEmbPointerField( + NDRConfPacketListField( + "Buffer", [], LPSERVER_ALIAS_INFO_0, size_is=lambda pkt: pkt.EntriesRead + ) + ), + ] + + +class LPSERVER_ALIAS_ENUM_STRUCT(NDRPacket): + ALIGNMENT = (4, 8) + fields_desc = [ + NDRIntField("Level", 0), + NDRUnionField( + [ ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1544(), LPSERVER_INFO_1544 + "ServerAliasInfo", + SERVER_ALIAS_INFO_0_CONTAINER(), + SERVER_ALIAS_INFO_0_CONTAINER, ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1544), - (lambda _, val: val.tag == 1544), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), - ), + ) + ], + StrFixedLenField("ServerAliasInfo", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerAliasEnum_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRPacketField( + "InfoStruct", LPSERVER_ALIAS_ENUM_STRUCT(), LPSERVER_ALIAS_ENUM_STRUCT + ), + NDRIntField("PreferedMaximumLength", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + ] + + +class NetrServerAliasEnum_Response(NDRPacket): + fields_desc = [ + NDRPacketField( + "InfoStruct", LPSERVER_ALIAS_ENUM_STRUCT(), LPSERVER_ALIAS_ENUM_STRUCT + ), + NDRIntField("TotalEntries", 0), + NDRFullPointerField(NDRIntField("ResumeHandle", 0)), + NDRIntField("status", 0), + ] + + +class NetrServerAliasDel_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1545(), LPSERVER_INFO_1545 + "InfoStruct", LPSERVER_ALIAS_INFO_0(), LPSERVER_ALIAS_INFO_0 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1545), - (lambda _, val: val.tag == 1545), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), - ), + ) + ], + StrFixedLenField("InfoStruct", "", length=0), + align=(4, 8), + switch_fmt=("L", "L"), + ), + ] + + +class NetrServerAliasDel_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + +class NetrShareDelEx_Request(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRConfVarStrNullFieldUtf16("ServerName", "")), + NDRIntField("Level", 0), + NDRUnionField( + [ ( NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1546(), LPSERVER_INFO_1546 - ) + NDRPacketField("ShareInfo", LPSHARE_INFO_0(), LPSHARE_INFO_0) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1546), - (lambda _, val: val.tag == 1546), + (lambda pkt: getattr(pkt, "Level", None) == 0), + (lambda _, val: val.tag == 0), ), ), ( NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1547(), LPSERVER_INFO_1547 - ) + NDRPacketField("ShareInfo", LPSHARE_INFO_1(), LPSHARE_INFO_1) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1547), - (lambda _, val: val.tag == 1547), + (lambda pkt: getattr(pkt, "Level", None) == 1), + (lambda _, val: val.tag == 1), ), ), ( NDRFullPointerField( - NDRPacketField( - "InfoStruct", LPSERVER_INFO_1548(), LPSERVER_INFO_1548 - ) + NDRPacketField("ShareInfo", LPSHARE_INFO_2(), LPSHARE_INFO_2) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1548), - (lambda _, val: val.tag == 1548), + (lambda pkt: getattr(pkt, "Level", None) == 2), + (lambda _, val: val.tag == 2), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1549(), LPSERVER_INFO_1549 + "ShareInfo", LPSHARE_INFO_502_I(), LPSHARE_INFO_502_I ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1549), - (lambda _, val: val.tag == 1549), + (lambda pkt: getattr(pkt, "Level", None) == 502), + (lambda _, val: val.tag == 502), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1550(), LPSERVER_INFO_1550 + "ShareInfo", LPSHARE_INFO_1004(), LPSHARE_INFO_1004 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1550), - (lambda _, val: val.tag == 1550), + (lambda pkt: getattr(pkt, "Level", None) == 1004), + (lambda _, val: val.tag == 1004), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1552(), LPSERVER_INFO_1552 + "ShareInfo", LPSHARE_INFO_1006(), LPSHARE_INFO_1006 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1552), - (lambda _, val: val.tag == 1552), + (lambda pkt: getattr(pkt, "Level", None) == 1006), + (lambda _, val: val.tag == 1006), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1553(), LPSERVER_INFO_1553 + "ShareInfo", LPSHARE_INFO_1501_I(), LPSHARE_INFO_1501_I ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1553), - (lambda _, val: val.tag == 1553), + (lambda pkt: getattr(pkt, "Level", None) == 1501), + (lambda _, val: val.tag == 1501), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1554(), LPSERVER_INFO_1554 + "ShareInfo", LPSHARE_INFO_1005(), LPSHARE_INFO_1005 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1554), - (lambda _, val: val.tag == 1554), + (lambda pkt: getattr(pkt, "Level", None) == 1005), + (lambda _, val: val.tag == 1005), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1555(), LPSERVER_INFO_1555 + "ShareInfo", LPSHARE_INFO_501(), LPSHARE_INFO_501 ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1555), - (lambda _, val: val.tag == 1555), + (lambda pkt: getattr(pkt, "Level", None) == 501), + (lambda _, val: val.tag == 501), ), ), ( NDRFullPointerField( NDRPacketField( - "InfoStruct", LPSERVER_INFO_1556(), LPSERVER_INFO_1556 + "ShareInfo", LPSHARE_INFO_503_I(), LPSHARE_INFO_503_I ) ), ( - (lambda pkt: getattr(pkt, "Level", None) == 1556), - (lambda _, val: val.tag == 1556), + (lambda pkt: getattr(pkt, "Level", None) == 503), + (lambda _, val: val.tag == 503), ), ), ], - StrFixedLenField("InfoStruct", "", length=0), + StrFixedLenField("ShareInfo", "", length=0), align=(4, 8), switch_fmt=("L", "L"), ), - NDRIntField("status", 0), ] +class NetrShareDelEx_Response(NDRPacket): + fields_desc = [NDRIntField("status", 0)] + + SRVSVC_OPNUMS = { # 0: Opnum0NotUsedOnWire, # 1: Opnum1NotUsedOnWire, # 2: Opnum2NotUsedOnWire, @@ -1476,12 +4001,64 @@ class NetrServerGetInfo_Response(NDRPacket): # 5: Opnum5NotUsedOnWire, # 6: Opnum6NotUsedOnWire, # 7: Opnum7NotUsedOnWire, + 8: DceRpcOp(NetrConnectionEnum_Request, NetrConnectionEnum_Response), + 9: DceRpcOp(NetrFileEnum_Request, NetrFileEnum_Response), + 10: DceRpcOp(NetrFileGetInfo_Request, NetrFileGetInfo_Response), + 11: DceRpcOp(NetrFileClose_Request, NetrFileClose_Response), + 12: DceRpcOp(NetrSessionEnum_Request, NetrSessionEnum_Response), + 13: DceRpcOp(NetrSessionDel_Request, NetrSessionDel_Response), + 14: DceRpcOp(NetrShareAdd_Request, NetrShareAdd_Response), 15: DceRpcOp(NetrShareEnum_Request, NetrShareEnum_Response), 16: DceRpcOp(NetrShareGetInfo_Request, NetrShareGetInfo_Response), + 17: DceRpcOp(NetrShareSetInfo_Request, NetrShareSetInfo_Response), + 18: DceRpcOp(NetrShareDel_Request, NetrShareDel_Response), + 19: DceRpcOp(NetrShareDelSticky_Request, NetrShareDelSticky_Response), + 20: DceRpcOp(NetrShareCheck_Request, NetrShareCheck_Response), 21: DceRpcOp(NetrServerGetInfo_Request, NetrServerGetInfo_Response), + 22: DceRpcOp(NetrServerSetInfo_Request, NetrServerSetInfo_Response), + 23: DceRpcOp(NetrServerDiskEnum_Request, NetrServerDiskEnum_Response), + 24: DceRpcOp(NetrServerStatisticsGet_Request, NetrServerStatisticsGet_Response), + 25: DceRpcOp(NetrServerTransportAdd_Request, NetrServerTransportAdd_Response), + 26: DceRpcOp(NetrServerTransportEnum_Request, NetrServerTransportEnum_Response), + 27: DceRpcOp(NetrServerTransportDel_Request, NetrServerTransportDel_Response), + 28: DceRpcOp(NetrRemoteTOD_Request, NetrRemoteTOD_Response), # 29: Opnum29NotUsedOnWire, + 30: DceRpcOp(NetprPathType_Request, NetprPathType_Response), + 31: DceRpcOp(NetprPathCanonicalize_Request, NetprPathCanonicalize_Response), + 32: DceRpcOp(NetprPathCompare_Request, NetprPathCompare_Response), + 33: DceRpcOp(NetprNameValidate_Request, NetprNameValidate_Response), + 34: DceRpcOp(NetprNameCanonicalize_Request, NetprNameCanonicalize_Response), + 35: DceRpcOp(NetprNameCompare_Request, NetprNameCompare_Response), + 36: DceRpcOp(NetrShareEnumSticky_Request, NetrShareEnumSticky_Response), + 37: DceRpcOp(NetrShareDelStart_Request, NetrShareDelStart_Response), + 38: DceRpcOp(NetrShareDelCommit_Request, NetrShareDelCommit_Response), + 39: DceRpcOp(NetrpGetFileSecurity_Request, NetrpGetFileSecurity_Response), + 40: DceRpcOp(NetrpSetFileSecurity_Request, NetrpSetFileSecurity_Response), + 41: DceRpcOp(NetrServerTransportAddEx_Request, NetrServerTransportAddEx_Response), # 42: Opnum42NotUsedOnWire, - # 47: Opnum47NotUsedOnWire + 43: DceRpcOp(NetrDfsGetVersion_Request, NetrDfsGetVersion_Response), + 44: DceRpcOp( + NetrDfsCreateLocalPartition_Request, NetrDfsCreateLocalPartition_Response + ), + 45: DceRpcOp( + NetrDfsDeleteLocalPartition_Request, NetrDfsDeleteLocalPartition_Response + ), + 46: DceRpcOp( + NetrDfsSetLocalVolumeState_Request, NetrDfsSetLocalVolumeState_Response + ), + # 47: Opnum47NotUsedOnWire, + 48: DceRpcOp(NetrDfsCreateExitPoint_Request, NetrDfsCreateExitPoint_Response), + 49: DceRpcOp(NetrDfsDeleteExitPoint_Request, NetrDfsDeleteExitPoint_Response), + 50: DceRpcOp(NetrDfsModifyPrefix_Request, NetrDfsModifyPrefix_Response), + 51: DceRpcOp(NetrDfsFixLocalVolume_Request, NetrDfsFixLocalVolume_Response), + 52: DceRpcOp( + NetrDfsManagerReportSiteInfo_Request, NetrDfsManagerReportSiteInfo_Response + ), + 53: DceRpcOp(NetrServerTransportDelEx_Request, NetrServerTransportDelEx_Response), + 54: DceRpcOp(NetrServerAliasAdd_Request, NetrServerAliasAdd_Response), + 55: DceRpcOp(NetrServerAliasEnum_Request, NetrServerAliasEnum_Response), + 56: DceRpcOp(NetrServerAliasDel_Request, NetrServerAliasDel_Response), + 57: DceRpcOp(NetrShareDelEx_Request, NetrShareDelEx_Response), } register_dcerpc_interface( name="srvsvc", diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index 5f40ddec50e..4445de58f8c 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -239,16 +239,27 @@ def connect( ) if self.transport == DCERPC_Transport.NCACN_NP: # SMB + if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY: + smb_kwargs.setdefault("REQUIRE_ENCRYPTION", True) + elif self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_INTEGRITY: + smb_kwargs.setdefault("REQUIRE_SIGNATURE", True) + # We pack the socket into a SMB_RPC_SOCKET sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( - sock, ssp=self.ssp, **smb_kwargs + sock, + ssp=self.ssp, + **smb_kwargs, ) # If the endpoint is provided, connect to it. if endpoint is not None: self.open_smbpipe(endpoint) - self.sock = DceRpcSocket(sock, DceRpc5, **self.dcesockargs) + self.sock = DceRpcSocket( + sock, + DceRpc5, + **self.dcesockargs, + ) elif self.transport == DCERPC_Transport.NCACN_IP_TCP: self.sock = DceRpcSocket( sock, diff --git a/scapy/layers/netbios.py b/scapy/layers/netbios.py index 3d20a0ae65b..68e34c54e61 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -84,12 +84,33 @@ def post_build(self, p, pay): _NETBIOS_SUFFIXES = { - 0x4141: "workstation", - 0x4141 + 0x03: "messenger service", - 0x4141 + 0x200: "file server service", - 0x4141 + 0x10b: "domain master browser", - 0x4141 + 0x10c: "domain controller", - 0x4141 + 0x10e: "browser election service" + 0x4141 + 0x00: "Workstation Service", + 0x4141 + 0x01: "Messenger Service", + 0x4141 + 0x03: "Messenger service", + 0x4141 + 0x06: "RAS Server Service", + 0x4141 + 0x1B: "Exchange MTA", + 0x4141 + 0x1F: "NetDDE Service", + 0x4141 + 0x20: "File Server Service", + 0x4141 + 0x21: "RAS Client Service", + 0x4141 + 0x22: "Exchange Interchange Service", + 0x4141 + 0x23: "Exchange Store", + 0x4141 + 0x24: "Exchange Directory", + 0x4141 + 0x30: "Modern Sharing Server Service", + 0x4141 + 0x31: "Modern Sharing Client Service", + 0x4141 + 0x43: "SMS Client Remote Control", + 0x4141 + 0x44: "SMS Admin Remote Control Tool", + 0x4141 + 0x45: "SMS Client Remote Chat", + 0x4141 + 0x46: "SMS Client Remote Transfer", + 0x4141 + 0x4C: "DEC Pathworks TCP/IP Service", + 0x4141 + 0x52: "DEC Pathworks TCP/IP Service", + 0x4141 + 0x6A: "Exchange IMC", + 0x4141 + 0x87: "Exchange MTA", + 0x4141 + 0xBE: "Network Monitor Agent", + 0x4141 + 0xBF: "Network Monitor Apps", + 0x4141 + 0x10b: "Domain Master Browser", + 0x4141 + 0x10c: "Domain Controller", + 0x4141 + 0x10e: "Browser Election Service", + 0x4141 + 0x200: "File Server Service", } _NETBIOS_QRTYPES = { diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index fbba9a24e94..fd94cc6f2ec 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -29,6 +29,7 @@ ASN1F_SEQUENCE_OF, ) from scapy.asn1packet import ASN1_Packet +from scapy.config import crypto_validator from scapy.compat import bytes_base64 from scapy.error import log_runtime from scapy.fields import ( @@ -490,7 +491,7 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): "J", "NEGOTIATE_OEM_DOMAIN_SUPPLIED", # K "NEGOTIATE_OEM_WORKSTATION_SUPPLIED", # L - "r7", + "NEGOTIATE_LOCAL_CALL", "NEGOTIATE_ALWAYS_SIGN", # M "TARGET_TYPE_DOMAIN", # N "TARGET_TYPE_SERVER", # O @@ -590,8 +591,9 @@ class NTLM_NEGOTIATE(_NTLM_VARIANT_Packet, NTLM_Header): "Payload", OFFSET, [ - _NTLMStrField("DomainName", b""), - _NTLMStrField("WorkstationName", b""), + # "MUST be encoded using the OEM character set" + StrField("DomainName", b""), + StrField("WorkstationName", b""), ], ), ] @@ -1267,7 +1269,6 @@ class NTLMSSP(SSP): Common arguments: - :param auth_level: One of DCE_C_AUTHN_LEVEL :param USE_MIC: whether to use a MIC or not (default: True) :param NTLM_VALUES: a dictionary used to override the following values @@ -1295,6 +1296,7 @@ class NTLMSSP(SSP): if without domain) :param HASHNT: the password to use for NTLM auth :param PASSWORD: the password to use for NTLM auth + :param LOCAL: use local authentication (must be running locally on Windows) Server-only arguments: @@ -1335,6 +1337,7 @@ class CONTEXT(SSP.CONTEXT): "ServerDomain", ] + @crypto_validator def __init__(self, IsAcceptor, req_flags=None): self.state = NTLMSSP.STATE.INIT self.SessionKey = None @@ -1392,7 +1395,7 @@ def __init__( self.USE_MIC = False else: self.USE_MIC = USE_MIC - self.NTLM_VALUES = NTLM_VALUES + if UPN is not None: # Populate values used only in server mode. from scapy.layers.kerberos import _parse_upn @@ -1407,7 +1410,7 @@ def __init__( pass # Compute various netbios/fqdn names - self.DOMAIN_FQDN = DOMAIN_FQDN or "domain.local" + self.DOMAIN_FQDN = DOMAIN_FQDN or "WORKGROUP" self.DOMAIN_NB_NAME = ( DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] ) @@ -1415,6 +1418,7 @@ def __init__( self.COMPUTER_FQDN = COMPUTER_FQDN or ( self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN ) + self.NTLM_VALUES = NTLM_VALUES if IDENTITIES: self.IDENTITIES = { @@ -1542,6 +1546,7 @@ def GSS_Init_sec_context( if Context.state == self.STATE.INIT: # Client: negotiate + # Create a default token tok = NTLM_NEGOTIATE( VARIANT=self.VARIANT, @@ -1594,15 +1599,18 @@ def GSS_Init_sec_context( ), ProductMajorVersion=10, ProductMinorVersion=0, - ProductBuild=19041, + ProductBuild=26100, ) + + # Update that token with the customs one if self.NTLM_VALUES: - # Update that token with the customs one for key in [ "NegotiateFlags", "ProductMajorVersion", "ProductMinorVersion", "ProductBuild", + "DomainName", + "WorkstationName", ]: if key in self.NTLM_VALUES: setattr(tok, key, self.NTLM_VALUES[key]) @@ -1613,6 +1621,7 @@ def GSS_Init_sec_context( elif Context.state == self.STATE.CLI_SENT_NEGO: # Client: auth (token=challenge) chall_tok = input_token + if self.UPN is None or self.HASHNT is None: raise ValueError( "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " @@ -1654,11 +1663,12 @@ def GSS_Init_sec_context( NegotiateFlags=chall_tok.NegotiateFlags, ProductMajorVersion=10, ProductMinorVersion=0, - ProductBuild=19041, + ProductBuild=26100, ) - tok.LmChallengeResponse = LMv2_RESPONSE() # Populate the token + tok.LmChallengeResponse = LMv2_RESPONSE() + # 1. Set username try: tok.UserName, realm = _parse_upn(self.UPN) @@ -2132,7 +2142,7 @@ class NTLMSSP_DOMAIN(NTLMSSP): :param HASHNT: the HASHNT of the machine account (use Netlogon secure channel). :param ssp: a KerberosSSP to use (use Kerberos secure channel). :param PASSWORD: the PASSWORD of the machine account to use for Netlogon. - :param DC_IP: (optional) specify the IP of the DC. + :param DC_FQDN: (optional) specify the FQDN of the DC. Netlogon example:: diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index cf2ee2e868a..5b4e5678c4b 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -943,7 +943,7 @@ class NETLOGON_LOGON_QUERY(NETLOGON): LEShortEnumField("OpCode", 0x7, _NETLOGON_opcodes), StrNullField("ComputerName", ""), StrNullField("MailslotName", ""), - StrNullFieldUtf16("UnicodeComputerName", ""), + ReversePadField(StrNullFieldUtf16("UnicodeComputerName", ""), 2), FlagsField("NtVersion", 0xB, -32, _NV_VERSION), XLEShortField("LmNtToken", 0xFFFF), XLEShortField("Lm20Token", 0xFFFF), @@ -1151,7 +1151,38 @@ class BRWS_HostAnnouncement(BRWS): StrFixedLenField("ServerName", b"", length=16), ByteField("OSVersionMajor", 6), ByteField("OSVersionMinor", 1), - LEIntField("ServerType", 4611), + FlagsField("ServerType", 4611, -32, { + 0x00000001: "SV_TYPE_WORKSTATION", + 0x00000002: "SV_TYPE_SERVER", + 0x00000004: "SV_TYPE_SQLSERVER", + 0x00000008: "SV_TYPE_DOMAIN_CTRL", + 0x00000010: "SV_TYPE_DOMAIN_BAKCTRL", + 0x00000020: "SV_TYPE_TIME_SOURCE", + 0x00000040: "SV_TYPE_AFP", + 0x00000080: "SV_TYPE_NOVELL", + 0x00000100: "SV_TYPE_DOMAIN_MEMBER", + 0x00000200: "SV_TYPE_PRINTQ_SERVER", + 0x00000400: "SV_TYPE_DIALIN_SERVER", + 0x00000800: "SV_TYPE_SERVER_UNIX,", + 0x00001000: "SV_TYPE_NT", + 0x00002000: "SV_TYPE_WFW", + 0x00004000: "SV_TYPE_SERVER_MFPN", + 0x00008000: "SV_TYPE_SERVER_NT", + 0x00010000: "SV_TYPE_POTENTIAL_BROWSER", + 0x00020000: "SV_TYPE_BACKUP_BROWSER", + 0x00040000: "SV_TYPE_MASTER_BROWSER", + 0x00080000: "SV_TYPE_DOMAIN_MASTER", + 0x00400000: "SV_TYPE_WINDOWS", + 0x00800000: "SV_TYPE_DFS", + 0x01000000: "SV_TYPE_CLUSTER_NT", + 0x02000000: "SV_TYPE_TERMINALSERVER", + 0x04000000: "SV_TYPE_CLUSTER_VS_NT", + 0x10000000: "SV_TYPE_DCE", + 0x20000000: "SV_TYPE_ALTERNATE_XPORT", + 0x40000000: "SV_TYPE_LOCAL_LIST_ONLY", + 0x80000000: "SV_TYPE_DOMAIN_ENUM", + 0xFFFFFFFF: "SV_TYPE_ALL", + }), ByteField("BrowserConfigVersionMajor", 21), ByteField("BrowserConfigVersionMinor", 1), XLEShortField("Signature", 0xAA55), diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 3b806d6fcdd..975b670a6eb 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -15,9 +15,10 @@ import io import os import pathlib +import re import socket -import time import threading +import time from scapy.automaton import ATMT, Automaton, ObjectPipe from scapy.config import conf @@ -33,13 +34,17 @@ from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface from scapy.layers.gssapi import ( + GSS_C_FLAGS, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, - GSS_C_FLAGS, + GSS_S_DEFECTIVE_TOKEN, ) from scapy.layers.msrpce.raw.ms_srvs import ( LPSHARE_ENUM_STRUCT, + NetrSessionEnum_Request, NetrShareEnum_Request, + PSESSION_ENUM_STRUCT, + SESSION_INFO_502_CONTAINER, SHARE_INFO_1_CONTAINER, ) from scapy.layers.ntlm import ( @@ -75,6 +80,7 @@ SMB2_ENCRYPTION_CIPHERS, SMB2_Encryption_Capabilities, SMB2_Error_Response, + SMB2_FILEID, SMB2_Header, SMB2_IOCTL_Request, SMB2_IOCTL_Response, @@ -220,7 +226,12 @@ def send(self, pkt): Length = len(pkt.payload.Data) elif typ == SMB2_IOCTL_Request: # [MS-SMB2] 3.3.5.15 - Length = max(len(pkt.payload.Input), pkt.payload.MaxOutputResponse) + try: + Length = max( + len(pkt.payload.Input), pkt.payload.MaxOutputResponse + ) + except AttributeError: + Length = 0 elif typ in [ SMB2_Query_Directory_Request, SMB2_Change_Notify_Request, @@ -238,7 +249,7 @@ def send(self, pkt): # [MS-SMB2] note <110> # "The Windows-based client will request credits up to a configurable # maximum of 128 by default." - pkt.CreditRequest = self.MaxCreditCount - self.CurrentCreditCount + pkt.CreditRequest = max(self.MaxCreditCount - self.CurrentCreditCount, 0) # Get first available message ID: [MS-SMB2] 3.2.4.1.3 and 3.2.4.1.5 pkt.MID = self.SequenceWindow[0] return super(SMB_Client, self).send(pkt) @@ -482,10 +493,20 @@ def update_smbheader(self, pkt): # DEV: add a condition on NEGOTIATED with prio=0 @ATMT.condition(NEGOTIATED, prio=1) + def should_retry_without_blob(self, ssp_tuple): + _, _, status = ssp_tuple + if status == GSS_S_DEFECTIVE_TOKEN: + # Token was defective. This could be that we passed a SPNEGO initial token + # to a NTLM SSP (not using SPNEGO). Retry using no input blob + raise self.NEGOTIATED() + + @ATMT.condition(NEGOTIATED, prio=2) def should_send_session_setup_request(self, ssp_tuple): _, _, status = ssp_tuple if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: - raise ValueError("Internal error: the SSP completed with an error.") + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) @ATMT.state() @@ -619,7 +640,9 @@ def AUTHENTICATED(self, ssp_blob=None): target_name="cifs/" + self.HOST if self.HOST else None, ) if status != GSS_S_COMPLETE: - raise ValueError("Internal error: the SSP completed with an error.") + raise ValueError( + "Internal error: the SSP completed with error: %s" % status + ) # Authentication was successful self.session.computeSMBSessionKeys(IsClient=True) @@ -738,6 +761,7 @@ def tree_connect(self, name): ) ] ), + chainCC=True, verbose=False, timeout=self.timeout, ) @@ -761,6 +785,7 @@ def tree_disconnect(self): SMB2_Tree_Disconnect_Request(), verbose=False, timeout=self.timeout, + chainCC=True, ) if not resp: raise ValueError("TreeDisconnect timed out !") @@ -873,6 +898,7 @@ def create_request( RequestedOplockLevel=RequestedOplockLevel, Name=name, ), + chainCC=True, verbose=0, timeout=self.timeout, ) @@ -887,7 +913,7 @@ def close_request(self, FileId): Close the FileId """ pkt = SMB2_Close_Request(FileId=FileId) - resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout, chainCC=True) if not resp: raise ValueError("CloseRequest timed out !") if SMB2_Close_Response not in resp: @@ -903,6 +929,7 @@ def read_request(self, FileId, Length, Offset=0): Length=Length, Offset=Offset, ), + chainCC=True, verbose=0, timeout=self.timeout * 10, ) @@ -922,6 +949,7 @@ def write_request(self, Data, FileId, Offset=0): Data=Data, Offset=Offset, ), + chainCC=True, verbose=0, timeout=self.timeout * 10, ) @@ -944,7 +972,7 @@ def query_directory(self, FileId, FileName="*"): FileName=FileName, Flags=Flags, ) - resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout, chainCC=True) Flags = 0 # only the first one is RESTART_SCANS if not resp: raise ValueError("QueryDirectory timed out !") @@ -977,13 +1005,31 @@ def query_info(self, FileId, InfoType, FileInfoClass, AdditionalInformation=0): FileId=FileId, AdditionalInformation=AdditionalInformation, ) - resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) + resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout, chainCC=True) if not resp: raise ValueError("QueryInfo timed out !") if SMB2_Query_Info_Response not in resp: raise ValueError("Failed QueryInfo ! %s" % resp.NTStatus) return resp.Output + def ioctl(self, CtlCode, Flags="SMB2_0_IOCTL_IS_FSCTL", Input=None): + """ + Perform an IOCTL + """ + pkt = SMB2_IOCTL_Request( + FileId=SMB2_FILEID( + Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF + ), + Flags=Flags, + CtlCode=CtlCode, + ) + if Input is not None: + pkt.Input = bytes(Input) + resp = self.ins.sr1(pkt, verbose=0, chainCC=True) + if SMB2_IOCTL_Response not in resp: + raise ValueError("Failed reading IOCTL_Response ! %s" % resp.NTStatus) + return resp.Output + def changenotify(self, FileId): """ Register change notify @@ -1022,16 +1068,16 @@ def close_pipe(self): self.close_request(self.PipeFileId) self.PipeFileId = None - def send(self, x): - """ - Internal ObjectPipe function. - """ - # Reminder: this class is an ObjectPipe, it's just a queue. + def send(self, x, is_sr1=True): + # Reminder: this class is an ObjectPipe ! It doesn't act as a real socket + # but just a queue. When someone calls the "send" function, they pipe + # some data that we must send, and tell us if they expect an answer through + # the is_sr1 flag. # Detect if DCE/RPC is fragmented. Then we must use Read/Write is_frag = x.pfc_flags & 3 != 3 - if self.use_ioctl and not is_frag and self.session.Dialect >= 0x0210: + if self.use_ioctl and is_sr1 and not is_frag and self.session.Dialect >= 0x0210: # Use IOCTLRequest pkt = SMB2_IOCTL_Request( FileId=self.PipeFileId, @@ -1039,11 +1085,12 @@ def send(self, x): CtlCode="FSCTL_PIPE_TRANSCEIVE", ) pkt.Input = bytes(x) - resp = self.ins.sr1(pkt, verbose=0) + resp = self.ins.sr1(pkt, verbose=0, chainCC=True) if SMB2_IOCTL_Response not in resp: raise ValueError("Failed reading IOCTL_Response ! %s" % resp.NTStatus) data = bytes(resp.Output) super(SMB_RPC_SOCKET, self).send(data) + # Handle BUFFER_OVERFLOW (big DCE/RPC response) while resp.NTStatus == "STATUS_BUFFER_OVERFLOW" or data[3] & 2 != 2: # Retrieve DCE/RPC full size @@ -1065,9 +1112,15 @@ def send(self, x): resp = self.ins.sr1(pkt, verbose=0) if SMB2_Write_Response not in resp: raise ValueError("Failed sending WriteResponse ! %s" % resp.NTStatus) + + # We may not be expecting an answer + if not is_sr1: + return + # If fragmented, only read if it's the last. if is_frag and not x.pfc_flags.PFC_LAST_FRAG: return + # We send a Read Request afterwards resp = self.ins.sr1( SMB2_Read_Request( @@ -1109,6 +1162,10 @@ class smbclient(CLIUtil): :param HashNt: if provided, used for auth (NTLM) :param HashAes256Sha96: if provided, used for auth (Kerberos) :param HashAes128Sha96: if provided, used for auth (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. :param ST: if provided, the service ticket to use (Kerberos) :param KEY: if provided, the session key associated to the ticket (Kerberos) :param cli: CLI mode (default True). False to use for scripting @@ -1130,6 +1187,7 @@ def __init__( HashAes256Sha96: bytes = None, HashAes128Sha96: bytes = None, use_krb5ccname: bool = False, + use_winssp: bool = False, port: int = 445, timeout: int = 5, debug: int = 0, @@ -1143,7 +1201,9 @@ def __init__( ): if cli: self._depcheck() - assert UPN or ssp or guest, "Either UPN, ssp or guest must be provided !" + assert ( + UPN or ssp or guest or use_winssp + ), "Either UPN, ssp or guest must be provided !" # Do we need to build a SSP? if ssp is None: # Create the SSP (only if not guest mode) @@ -1159,6 +1219,7 @@ def __init__( KEY=KEY, kerberos_required=kerberos_required, use_krb5ccname=use_krb5ccname, + use_winssp=use_winssp, ) else: # Guest mode @@ -1632,6 +1693,35 @@ def _send_file(self, fname, fd): self.smbsock.close_request(fileId) return offset + def _listr(self, directory, regx=None, max_depth=None, depth=0, _verb=True): + """ + Internal recursive function that yields the remote files + """ + if max_depth is not None and depth > max_depth: + return + for x in self.ls(parent=directory): + if x[0] in [".", ".."]: + # Discard . and .. + continue + remote = directory / x[0] + try: + if x[1].FILE_ATTRIBUTE_DIRECTORY: + # Sub-directory + yield from self._listr( + remote, + regx=regx, + depth=depth + 1, + max_depth=max_depth, + _verb=_verb, + ) + else: + # Sub-file + if regx is None or regx.match(str(remote)): + yield remote + except ValueError as ex: + if _verb: + print(conf.color_theme.red(directory), "->", str(ex)) + def _getr(self, directory, _root, _verb=True): """ Internal recursive function to get a directory @@ -1683,6 +1773,8 @@ def get(self, file, _dest=None, _verb=True, *, r=False): # Write the buffer if _dest is None: _dest = self.localpwd / fname + if not _dest.parent.exists(): + _dest.parent.mkdir() with _dest.open("wb") as fd: size = self._get_file(file, fd) return fname, size @@ -1703,6 +1795,103 @@ def get_complete(self, file): return [] return self._fs_complete(file) + @CLIUtil.addcommand() + def tree(self, regx: str = None, max_depth=None): + """ + Show the tree of files from the current directory + """ + if self._require_share(): + return + if regx is not None: + regx = re.compile(regx) + if max_depth is not None: + max_depth = int(max_depth) + return self._listr(self.pwd, regx=regx, max_depth=max_depth) + + @CLIUtil.addoutput(tree) + def tree_output(self, result): + """ + Print the output of 'tree' + """ + if self._require_share(silent=True): + return + for file in result: + print(str(file), flush=True) + + @CLIUtil.addcommand() + def server(self, command): + """ + Get information about the remote server + """ + if command == "network": + # We get the list of interfaces + if self._require_share(): + return + resp = self.smbsock.ioctl(CtlCode="FSCTL_QUERY_NETWORK_INTERFACE_INFO") + return (command, resp) + elif command == "sessions": + # We get the list of active connections via RPC + self.rpcclient.open_smbpipe("srvsvc") + self.rpcclient.bind(find_dcerpc_interface("srvsvc")) + ResumeHandle = None + results = [] + while True: + req = NetrSessionEnum_Request( + ClientName=None, + UserName=None, + InfoStruct=PSESSION_ENUM_STRUCT( + Level=502, + SessionInfo=NDRUnion( + tag=502, + value=SESSION_INFO_502_CONTAINER(Buffer=None), + ), + ), + PreferedMaximumLength=0xFFFFFFFF, + ResumeHandle=ResumeHandle, + ) + resp = self.rpcclient.sr1_req(req, timeout=self.timeout) + resp.show() + ResumeHandle = resp.ResumeHandle + if resp.status in [0, 0x000000EA]: # ERROR_SUCCESS / ERROR_MORE_DATA + results.extend(resp.valueof("InfoStruct.SessionInfo.Buffer")) + if resp.status == 0: + break + else: + break + self.rpcclient.close_smbpipe() + return (command, results) + else: + raise ValueError("Unknown server command: %s" % command) + + @CLIUtil.addoutput(server) + def server_output(self, result): + """ + Print the output of 'server' + """ + command, result = result + if command == "network": + # Show the various interfaces, their IP and capabilities + result.interfaces = [x for x in result.interfaces if x.IfIndex != 0] + result.show() + elif command == "sessions": + # Show the list of active sessions + for sess in result: + print("- cname:", sess.valueof("sesi502_cname")) + print(" username:", sess.valueof("sesi502_username")) + print(" num_opens:", sess.valueof("sesi502_num_opens")) + print(" time:", sess.valueof("sesi502_time")) + print(" idle_time:", sess.valueof("sesi502_idle_time")) + print(" user_flags:", sess.valueof("sesi502_user_flags")) + print(" cltype_name:", sess.valueof("sesi502_cltype_name")) + print(" transport:", sess.valueof("sesi502_transport")) + + @CLIUtil.addcomplete(server) + def server_complete(self, line): + """ + Auto-complete server + """ + return ["network", "sessions"] + @CLIUtil.addcommand(mono=True, globsupport=True) def cat(self, file): """ diff --git a/scapy/layers/snmp.py b/scapy/layers/snmp.py index d6a8bf69e56..713a65a46b8 100644 --- a/scapy/layers/snmp.py +++ b/scapy/layers/snmp.py @@ -15,7 +15,7 @@ from scapy.asn1.asn1 import ASN1_Class_UNIVERSAL, ASN1_Codecs, ASN1_NULL, \ ASN1_SEQUENCE from scapy.asn1.ber import BERcodec_SEQUENCE -from scapy.sendrecv import sr1 +from scapy.sendrecv import sr, sr1 from scapy.volatile import RandShort, IntAutoTime from scapy.layers.inet import UDP, IP, ICMP @@ -287,10 +287,51 @@ def answers(self, other): bind_layers(UDP, SNMP, sport=161, dport=161) +def snmpget(dst, oid="1.0.8802.1.1.1.1.1.2.1.2.29", community="public"): + """ + SNMP get. + + This can be used to perform a SNMP scan:: + + >>> snmpget("192.168.0.0/16", community="public") + """ + ans, _ = sr( + IP(dst=dst) / UDP(sport=RandShort()) / SNMP( + community=community, + PDU=SNMPnext(varbindlist=[SNMPvarbind(oid=oid)]), + ), + timeout=2, + chainCC=1, + verbose=0, + retry=2, + ) + for r in ans: + if ICMP in r.answer: + print(repr(r.answer)) + return + print("[%-10s] %-40s: %r" % ( + r.query.dst, + r.answer[SNMPvarbind].oid.val, + r.answer[SNMPvarbind].value, + )) + + def snmpwalk(dst, oid="1", community="public"): + """ + SNMP walk + """ try: while True: - r = sr1(IP(dst=dst) / UDP(sport=RandShort()) / SNMP(community=community, PDU=SNMPnext(varbindlist=[SNMPvarbind(oid=oid)])), timeout=2, chainCC=1, verbose=0, retry=2) # noqa: E501 + r = sr1( + IP(dst=dst) / UDP(sport=RandShort()) / SNMP( + community=community, + PDU=SNMPnext(varbindlist=[SNMPvarbind(oid=oid)]), + ), + timeout=2, + chainCC=1, + verbose=0, + retry=2, + ) if r is None: print("No answers") break diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index 9f498711c0c..7073d907295 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -40,6 +40,7 @@ ASN1F_STRING, ) from scapy.asn1packet import ASN1_Packet +from scapy.consts import WINDOWS from scapy.fields import ( FieldListField, LEIntEnumField, @@ -724,10 +725,13 @@ def from_cli_arguments( ccache: str = None, debug: int = 0, use_krb5ccname: bool = False, + use_winssp: bool = False, ): """ Initialize a SPNEGOSSP from a list of many arguments. - This is useful in a CLI, with NTLM and Kerberos supported by default. + + This is useful in a CLI, as it will try to build the best SPNEGOSSP + with NTLM and Kerberos based on the various parameters. :param UPN: the UPN of the user to use. :param target: the target IP/hostname entered by the user. @@ -743,17 +747,27 @@ def from_cli_arguments( :param ccache: (str) if provided, a path to a CCACHE (Kerberos) :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. """ kerberos = True hostname = None # Check if target is a hostname / Check IP - if ":" in target: + if target and ":" in target: if not valid_ip6(target): hostname = target else: if not valid_ip(target): hostname = target + # If using WinSSP, this goes fast. + if use_winssp: + if not WINDOWS: + raise OSError("Cannot use WinSSP on a non-Windows computer !") + from scapy.arch.windows.sspi import WinSSP + + return WinSSP() + # Check UPN try: _, realm = _parse_upn(UPN) diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index 7f3e710d806..e41ca71a784 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -135,10 +135,10 @@ X509_CRL, X509_SubjectPublicKeyInfo, ) +from scapy.layers.tls.crypto.hash import _get_hash from scapy.layers.tls.crypto.pkcs1 import ( _DecryptAndSignRSA, _EncryptAndVerifyRSA, - _get_hash, pkcs_os2ip, ) from scapy.compat import bytes_encode @@ -155,7 +155,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519 + from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519, x448 # cryptography raised the minimum RSA key length to 1024 in 43.0+ # https://github.com/pyca/cryptography/pull/10278 @@ -567,15 +567,6 @@ def __call__(cls, key_path=None, cryptography_obj=None): _an RSAPrivateKey; _an ECDSAPrivateKey. """ - if key_path is None: - obj = type.__call__(cls) - if cls is PrivKey: - cls = PrivKeyECDSA - obj.__class__ = cls - obj.frmt = "original" - obj.fill_and_store() - return obj - # This allows to import cryptography objects directly if cryptography_obj is not None: # We (stupidly) need to go through the whole import process because RSA @@ -588,6 +579,14 @@ def __call__(cls, key_path=None, cryptography_obj=None): encryption_algorithm=serialization.NoEncryption(), ), ) + elif key_path is None: + obj = type.__call__(cls) + if cls is PrivKey: + cls = PrivKeyECDSA + obj.__class__ = cls + obj.frmt = "original" + obj.fill_and_store() + return obj else: # Load from file obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) @@ -1029,7 +1028,7 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubkey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) - def getSignatureHash(self): + def getCertSignatureHash(self): """ Return the hash cryptography object used by the 'signatureAlgorithm' """ @@ -1142,6 +1141,10 @@ def pubKey(self): ) return self.pubkey + @property + def extensions(self): + return self.tbsCertificate.extensions + def __eq__(self, other): return self.der == other.der @@ -1635,7 +1638,7 @@ def _rec_getchain(chain, curtree): for c, subtree in curtree: curchain = chain + [c] # If 'cert' is issued by c - if cert.isIssuer(c): + if cert.isIssuer(c) or c == cert: # Final node of the chain ! # (add the final cert if not self signed) if c != cert: @@ -1650,7 +1653,8 @@ def _rec_getchain(chain, curtree): chain = _rec_getchain([], self.tree) if chain is not None: - return CertTree(chain) + # We add the first certificate to the ROOT in all cases + return CertTree(chain, [chain[0]]) else: return None @@ -1718,13 +1722,75 @@ def __init__( self.store = store self.crls = crls + def _get_algorithms(self, key: PrivKey, h="sha256") -> ASN1_OID: + """ + Get the algorithms matching a private key + """ + if isinstance(key, PrivKeyRSA): + # RFC3370 sect 3.2 + return ( + ASN1_OID("rsaEncryption"), + _get_hash(h), + h, + ) + elif isinstance(key, PrivKeyECDSA): + # RFC5753 sect 2.1.1 + if h == "sha1": + return ( + ASN1_OID("ecdsa-with-SHA1"), + hashes.SHA1(), + "sha1", + ) + elif h == "sha224": + return ( + ASN1_OID("ecdsa-with-SHA224"), + hashes.SHA224(), + "sha224", + ) + elif h == "sha256": + return ( + ASN1_OID("ecdsa-with-SHA256"), + hashes.SHA256(), + "sha256", + ) + elif h == "sha384": + return ( + ASN1_OID("ecdsa-with-SHA384"), + hashes.SHA384(), + "sha384", + ) + elif h == "sha512": + return ( + ASN1_OID("ecdsa-with-SHA512"), + hashes.SHA512(), + "sha512", + ) + else: + raise ValueError("Unknown hash for private key !") + elif isinstance(key, PrivKeyEdDSA): + # RFC8419 sect 2.3 + if isinstance(key.key, x25519.X25519PrivateKey): + return ( + ASN1_OID("Ed25519"), + hashes.SHA512(), + "sha512", + ) + elif isinstance(key.key, x448.X448PrivateKey): + return ( + ASN1_OID("Ed448"), + hashes.SHAKE256(64), + "shake256", + ) + else: + raise ValueError("Unknown private key type !") + def sign( self, message: Union[bytes, Packet], eContentType: ASN1_OID, cert: Cert, key: PrivKey, - h: Optional[str] = None, + dhash: Optional[str] = "sha256", ): """ Sign a message using CMS. @@ -1733,17 +1799,18 @@ def sign( :param eContentType: the OID of the inner content. :param cert: the certificate whose key to use use for signing. :param key: the private key to use for signing. - :param h: the hash to use (default: same as the certificate's signature) + :param dhash: the hash to use for message digest (ECDSA only). We currently only support X.509 certificates ! """ - # RFC3852 - 5.4. Message Digest Calculation Process - h = h or _get_cert_sig_hashname(cert) - hash = hashes.Hash(_get_hash(h)) + sigalg, cdhash, dhash = self._get_algorithms(key, h=dhash) + + # RFC3852 5.4. Message Digest Calculation Process + hash = hashes.Hash(cdhash) hash.update(bytes(message)) hashed_message = hash.finalize() - # 5.5. Signature Generation Process + # RFC3852 5.5. Signature Generation Process signerInfo = CMS_SignerInfo( version=1, sid=CMS_IssuerAndSerialNumber( @@ -1751,7 +1818,7 @@ def sign( serialNumber=cert.tbsCertificate.serialNumber, ), digestAlgorithm=X509_AlgorithmIdentifier( - algorithm=ASN1_OID(h), + algorithm=ASN1_OID(dhash), parameters=ASN1_NULL(0), ), signedAttrs=[ @@ -1769,7 +1836,10 @@ def sign( ], ), ], - signatureAlgorithm=cert.tbsCertificate.signature, + signatureAlgorithm=X509_AlgorithmIdentifier( + algorithm=sigalg, + parameters=ASN1_NULL(0), + ), ) signerInfo.signature = ASN1_STRING( key.sign( @@ -1778,7 +1848,7 @@ def sign( signedAttrs=signerInfo.signedAttrs, ) ), - h=h, + h=dhash, ) ) @@ -1792,7 +1862,7 @@ def sign( content=CMS_SignedData( version=3 if certificates else 1, digestAlgorithms=X509_AlgorithmIdentifier( - algorithm=ASN1_OID(h), + algorithm=ASN1_OID(dhash), parameters=ASN1_NULL(0), ), encapContentInfo=CMS_EncapsulatedContentInfo( @@ -1820,6 +1890,7 @@ def verify( contentInfo: CMS_ContentInfo, eContentType: Optional[ASN1_OID] = None, eContent: Optional[bytes] = None, + no_verify_cert: bool = False, ): """ Verify a CMS message against the list of trusted certificates, @@ -1828,6 +1899,7 @@ def verify( :param contentInfo: the ContentInfo whose signature to verify :param eContentType: if provided, verifies that the content type is valid :param eContent: in PKCS 7.1, provide the content to verify + :param no_verify_cert: do not check the remote certificate (unsafe) """ if contentInfo.contentType.oidname != "id-signedData": raise ValueError("ContentInfo isn't signed !") @@ -1846,11 +1918,14 @@ def verify( # Check all signatures for signerInfo in signeddata.signerInfos: + sigh = hash_by_oid[signerInfo.signatureAlgorithm.algorithm.val] + # Find certificate in the chain that did this cert: Cert = certTree.findCertBySid(signerInfo.sid) # Verify certificate signature - certTree.verify(cert) + if not no_verify_cert: + certTree.verify(cert) # Verify the message hash if signerInfo.signedAttrs: @@ -1911,11 +1986,13 @@ def verify( ) ), sig=signerInfo.signature.val, + h=sigh, ) else: cert.verify( msg=bytes(signeddata.encapContentInfo), sig=signerInfo.signature.val, + h=sigh, ) # Return the content diff --git a/scapy/layers/tls/crypto/h_mac.py b/scapy/layers/tls/crypto/h_mac.py index 26c69ebfbe0..db984dc22a2 100644 --- a/scapy/layers/tls/crypto/h_mac.py +++ b/scapy/layers/tls/crypto/h_mac.py @@ -8,10 +8,12 @@ HMAC classes. """ -import hmac - +from scapy.config import conf from scapy.layers.tls.crypto.hash import _tls_hash_algs -from scapy.compat import bytes_encode + +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.hmac import HMAC _SSLv3_PAD1_MD5 = b"\x36" * 48 _SSLv3_PAD1_SHA1 = b"\x36" * 40 @@ -30,15 +32,18 @@ class _GenericHMACMetaclass(type): the associated hash function (see RFC 5246, appendix C). Also, we do not need to instantiate the associated hash function. """ + def __new__(cls, hmac_name, bases, dct): - hash_name = hmac_name[5:] # remove leading "Hmac_" + hash_name = hmac_name[5:] # remove leading "Hmac_" if hmac_name != "_GenericHMAC": + hash_alg = _tls_hash_algs[hash_name.lower()] dct["name"] = "HMAC-%s" % hash_name - dct["hash_alg"] = _tls_hash_algs[hash_name] - dct["hmac_len"] = _tls_hash_algs[hash_name].hash_len + dct["hash_alg"] = hash_alg + dct["hmac_len"] = hash_alg.hash_len dct["key_len"] = dct["hmac_len"] - the_class = super(_GenericHMACMetaclass, cls).__new__(cls, hmac_name, - bases, dct) + the_class = super(_GenericHMACMetaclass, cls).__new__( + cls, hmac_name, bases, dct + ) if hmac_name != "_GenericHMAC": _tls_hmac_algs[dct["name"]] = the_class return the_class @@ -48,38 +53,36 @@ class HMACError(Exception): """ Raised when HMAC verification fails. """ + pass class _GenericHMAC(metaclass=_GenericHMACMetaclass): def __init__(self, key=None): - if key is None: - self.key = b"" - else: - self.key = bytes_encode(key) + self.key = key or b"" def digest(self, tbd): if self.key is None: raise HMACError - tbd = bytes_encode(tbd) - return hmac.new(self.key, tbd, self.hash_alg.hash_cls).digest() + hm = HMAC(self.key, self.hash_alg.hash_cls(), backend=default_backend()) + hm.update(tbd) + return hm.finalize() def digest_sslv3(self, tbd): if self.key is None: raise HMACError h = self.hash_alg() - if h.name == "SHA": + if h.name == "sha": pad1 = _SSLv3_PAD1_SHA1 pad2 = _SSLv3_PAD2_SHA1 - elif h.name == "MD5": + elif h.name == "md5": pad1 = _SSLv3_PAD1_MD5 pad2 = _SSLv3_PAD2_MD5 else: raise HMACError("Provided hash does not work with SSLv3.") - return h.digest(self.key + pad2 + - h.digest(self.key + pad1 + tbd)) + return h.digest(self.key + pad2 + h.digest(self.key + pad1 + tbd)) class Hmac_NULL(_GenericHMAC): @@ -125,4 +128,4 @@ def Hmac(key, hashtype): """ Return Hmac object from Hash object and key """ - return _tls_hmac_algs[f"HMAC-{hashtype.name}"](key=key) + return _tls_hmac_algs[f"HMAC-{hashtype.name.upper()}"](key=key) diff --git a/scapy/layers/tls/crypto/hash.py b/scapy/layers/tls/crypto/hash.py index b1bcdbe2669..05c653538ab 100644 --- a/scapy/layers/tls/crypto/hash.py +++ b/scapy/layers/tls/crypto/hash.py @@ -8,9 +8,25 @@ Hash classes. """ -from hashlib import md5, sha1, sha224, sha256, sha384, sha512 +from scapy.config import conf, crypto_validator from scapy.layers.tls.crypto.md4 import MD4 as md4 +if conf.crypto_valid: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.hashes import ( + MD5, + SHA1, + SHA224, + SHA256, + SHA384, + SHA512, + SHAKE256, + ) + from cryptography.hazmat.primitives.hashes import HashAlgorithm +else: + MD5 = SHA1 = SHA224 = SHA256 = SHA384 = SHA512 = SHAKE256 = None + HashAlgorithm = object _tls_hash_algs = {} @@ -20,19 +36,23 @@ class _GenericHashMetaclass(type): Hash classes are automatically registered through this metaclass. Furthermore, their name attribute is extracted from their class name. """ + def __new__(cls, hash_name, bases, dct): if hash_name != "_GenericHash": - dct["name"] = hash_name[5:] # remove leading "Hash_" - the_class = super(_GenericHashMetaclass, cls).__new__(cls, hash_name, - bases, dct) + dct["name"] = hash_name[5:].lower() # remove leading "Hash_" + the_class = super(_GenericHashMetaclass, cls).__new__( + cls, hash_name, bases, dct + ) if hash_name != "_GenericHash": - _tls_hash_algs[hash_name[5:]] = the_class + _tls_hash_algs[dct["name"]] = the_class return the_class class _GenericHash(metaclass=_GenericHashMetaclass): def digest(self, tbd): - return self.hash_cls(tbd).digest() + digest = hashes.Hash(self.hash_cls(), backend=default_backend()) + digest.update(tbd) + return digest.finalize() class Hash_NULL(_GenericHash): @@ -46,32 +66,76 @@ class Hash_MD4(_GenericHash): hash_cls = md4 hash_len = 16 + def digest(self, tbd): + return self.hash_cls(tbd).digest() + class Hash_MD5(_GenericHash): - hash_cls = md5 + hash_cls = MD5 hash_len = 16 class Hash_SHA(_GenericHash): - hash_cls = sha1 + hash_cls = SHA1 hash_len = 20 +_tls_hash_algs["sha1"] = Hash_SHA + + class Hash_SHA224(_GenericHash): - hash_cls = sha224 + hash_cls = SHA224 hash_len = 28 class Hash_SHA256(_GenericHash): - hash_cls = sha256 + hash_cls = SHA256 hash_len = 32 class Hash_SHA384(_GenericHash): - hash_cls = sha384 + hash_cls = SHA384 hash_len = 48 class Hash_SHA512(_GenericHash): - hash_cls = sha512 + hash_cls = SHA512 hash_len = 64 + + +# first, we add the "md5-sha1" hash from openssl to python-cryptography +class MD5_SHA1(HashAlgorithm): + name = "md5-sha1" + digest_size = 36 + block_size = 64 + + +class Hash_MD5SHA1(_GenericHash): + hash_cls = MD5_SHA1 + hash_len = 36 + + +_tls_hash_algs["md5-sha1"] = Hash_MD5SHA1 + + +class Hash_SHAKE256(_GenericHash): + hash_cls = SHAKE256 + + def __init__(self, digest_size: int): + self.hash_len = digest_size + + def digest(self, tbd): + digest = hashes.Hash(self.hash_cls(self.hash_len), backend=default_backend()) + digest.update(tbd) + return digest.finalize() + + +@crypto_validator +def _get_hash(hashStr): + """ + Return a cryptography-hash by its name + """ + try: + return _tls_hash_algs[hashStr].hash_cls() + except KeyError: + raise KeyError("Unknown hash function %s" % hashStr) diff --git a/scapy/layers/tls/crypto/hkdf.py b/scapy/layers/tls/crypto/hkdf.py index 649305666a5..2d2af9d272b 100644 --- a/scapy/layers/tls/crypto/hkdf.py +++ b/scapy/layers/tls/crypto/hkdf.py @@ -10,7 +10,7 @@ import struct from scapy.config import conf, crypto_validator -from scapy.layers.tls.crypto.pkcs1 import _get_hash +from scapy.layers.tls.crypto.hash import _get_hash if conf.crypto_valid: from cryptography.hazmat.backends import default_backend diff --git a/scapy/layers/tls/crypto/pkcs1.py b/scapy/layers/tls/crypto/pkcs1.py index 18008a5e7d8..82082edc3de 100644 --- a/scapy/layers/tls/crypto/pkcs1.py +++ b/scapy/layers/tls/crypto/pkcs1.py @@ -16,12 +16,12 @@ from scapy.config import conf, crypto_validator from scapy.error import warning +from scapy.layers.tls.crypto.hash import _get_hash if conf.crypto_valid: from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding - from cryptography.hazmat.primitives.hashes import HashAlgorithm ##################################################################### @@ -89,31 +89,7 @@ def _legacy_pkcs1_v1_5_encode_md5_sha1(M, emLen): # Hash and padding helpers ##################################################################### -_get_hash = None if conf.crypto_valid: - - # first, we add the "md5-sha1" hash from openssl to python-cryptography - class MD5_SHA1(HashAlgorithm): - name = "md5-sha1" - digest_size = 36 - block_size = 64 - - _hashes = { - "md5": hashes.MD5, - "sha1": hashes.SHA1, - "sha224": hashes.SHA224, - "sha256": hashes.SHA256, - "sha384": hashes.SHA384, - "sha512": hashes.SHA512, - "md5-sha1": MD5_SHA1 - } - - def _get_hash(hashStr): - try: - return _hashes[hashStr]() - except KeyError: - raise KeyError("Unknown hash function %s" % hashStr) - def _get_padding(padStr, mgf=padding.MGF1, h=hashes.SHA256, label=None): if padStr == "pkcs": return padding.PKCS1v15() diff --git a/scapy/layers/tls/crypto/prf.py b/scapy/layers/tls/crypto/prf.py index 39f35509e54..d5e7e76f5b9 100644 --- a/scapy/layers/tls/crypto/prf.py +++ b/scapy/layers/tls/crypto/prf.py @@ -73,7 +73,7 @@ def _tls_P_SHA512(secret, seed, req_len): # PRF functions, according to the protocol version def _sslv2_PRF(secret, seed, req_len): - hash_md5 = _tls_hash_algs["MD5"]() + hash_md5 = _tls_hash_algs["md5"]() rounds = (req_len + hash_md5.hash_len - 1) // hash_md5.hash_len res = b"" @@ -108,8 +108,8 @@ def _ssl_PRF(secret, seed, req_len): b"M", b"N", b"O", b"P", b"Q", b"R", b"S", b"T", b"U", b"V", b"W", b"X", # noqa: E501 b"Y", b"Z"] res = b"" - hash_sha1 = _tls_hash_algs["SHA"]() - hash_md5 = _tls_hash_algs["MD5"]() + hash_sha1 = _tls_hash_algs["sha"]() + hash_md5 = _tls_hash_algs["md5"]() rounds = (req_len + hash_md5.hash_len - 1) // hash_md5.hash_len for i in range(rounds): @@ -185,7 +185,7 @@ class PRF(object): context of the connection state using the tls_version and the cipher suite. """ - def __init__(self, hash_name="SHA256", tls_version=0x0303): + def __init__(self, hash_name="sha256", tls_version=0x0303): self.tls_version = tls_version self.hash_name = hash_name @@ -197,13 +197,13 @@ def __init__(self, hash_name="SHA256", tls_version=0x0303): tls_version == 0x0302): # TLS 1.1 self.prf = _tls_PRF elif tls_version == 0x0303: # TLS 1.2 - if hash_name == "SHA384": + if hash_name == "sha384": self.prf = _tls12_SHA384PRF - elif hash_name == "SHA512": + elif hash_name == "sha512": self.prf = _tls12_SHA512PRF else: - if hash_name in ["MD5", "SHA"]: - self.hash_name = "SHA256" + if hash_name in ["md5", "sha"]: + self.hash_name = "sha256" self.prf = _tls12_SHA256PRF else: warning("Unknown TLS version") @@ -270,8 +270,8 @@ def compute_verify_data(self, con_end, read_or_write, sslv3_sha1_pad1 = b"\x36" * 40 sslv3_sha1_pad2 = b"\x5c" * 40 - md5 = _tls_hash_algs["MD5"]() - sha1 = _tls_hash_algs["SHA"]() + md5 = _tls_hash_algs["md5"]() + sha1 = _tls_hash_algs["sha"]() md5_hash = md5.digest(master_secret + sslv3_md5_pad2 + md5.digest(handshake_msg + label + @@ -290,8 +290,8 @@ def compute_verify_data(self, con_end, read_or_write, label = ("%s finished" % d[con_end]).encode() if self.tls_version <= 0x0302: - s1 = _tls_hash_algs["MD5"]().digest(handshake_msg) - s2 = _tls_hash_algs["SHA"]().digest(handshake_msg) + s1 = _tls_hash_algs["md5"]().digest(handshake_msg) + s2 = _tls_hash_algs["sha"]().digest(handshake_msg) verify_data = self.prf(master_secret, label, s1 + s2, 12) else: h = _tls_hash_algs[self.hash_name]() @@ -317,7 +317,7 @@ def postprocess_key_for_export(self, key, client_random, server_random, tbh = key + client_random + server_random else: tbh = key + server_random + client_random - export_key = _tls_hash_algs["MD5"]().digest(tbh)[:req_len] + export_key = _tls_hash_algs["md5"]().digest(tbh)[:req_len] else: if s: tag = b"client write key" @@ -346,7 +346,7 @@ def generate_iv_for_export(self, client_random, server_random, tbh = client_random + server_random else: tbh = server_random + client_random - iv = _tls_hash_algs["MD5"]().digest(tbh)[:req_len] + iv = _tls_hash_algs["md5"]().digest(tbh)[:req_len] else: iv_block = self.prf("", b"IV block", diff --git a/scapy/layers/tls/crypto/suites.py b/scapy/layers/tls/crypto/suites.py index f7079384d42..1626a442717 100644 --- a/scapy/layers/tls/crypto/suites.py +++ b/scapy/layers/tls/crypto/suites.py @@ -29,7 +29,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. if s.endswith("CCM") or s.endswith("CCM_8"): kx_name, s = s.split("_WITH_") kx_alg = _tls_kx_algs.get(kx_name) - hash_alg = _tls_hash_algs.get("SHA256") + hash_alg = _tls_hash_algs.get("sha256") cipher_alg = _tls_cipher_algs.get(s) hmac_alg = None @@ -42,7 +42,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. kx_alg = _tls_kx_algs.get("TLS13") hash_name = s.split('_')[-1] - hash_alg = _tls_hash_algs.get(hash_name) + hash_alg = _tls_hash_algs.get(hash_name.lower()) cipher_name = s[:-(len(hash_name) + 1)] if tls1_3: @@ -61,7 +61,7 @@ class and the HMAC class, through the parsing of the ciphersuite name. cipher_alg = _tls_cipher_algs.get(cipher_name.rstrip("_EXPORT40")) kx_alg.export = cipher_name.endswith("_EXPORT40") hmac_alg = _tls_hmac_algs.get("HMAC-NULL") - hash_alg = _tls_hash_algs.get(hash_name) + hash_alg = _tls_hash_algs.get(hash_name.lower()) return kx_alg, cipher_alg, hmac_alg, hash_alg, tls1_3 diff --git a/scapy/layers/windows/erref.py b/scapy/layers/windows/erref.py index b18cacf769e..f3a2440198f 100644 --- a/scapy/layers/windows/erref.py +++ b/scapy/layers/windows/erref.py @@ -17,7 +17,6 @@ 0x00000011: "ERROR_NOT_SAME_DEVICE", 0x00000013: "ERROR_WRITE_PROTECT", 0x00000057: "ERROR_INVALID_PARAMETER", - 0xC000006A: "STATUS_WRONG_PASSWORD", 0x0000007A: "ERROR_INSUFFICIENT_BUFFER", 0x0000007B: "ERROR_INVALID_NAME", 0x000000A1: "ERROR_BAD_PATHNAME", @@ -53,12 +52,14 @@ 0xC0000043: "STATUS_SHARING_VIOLATION", 0xC0000061: "STATUS_PRIVILEGE_NOT_HELD", 0xC0000064: "STATUS_NO_SUCH_USER", + 0xC000006A: "STATUS_WRONG_PASSWORD", 0xC000006D: "STATUS_LOGON_FAILURE", 0xC000006E: "STATUS_ACCOUNT_RESTRICTION", 0xC0000070: "STATUS_INVALID_WORKSTATION", 0xC0000071: "STATUS_PASSWORD_EXPIRED", 0xC0000072: "STATUS_ACCOUNT_DISABLED", 0xC000009A: "STATUS_INSUFFICIENT_RESOURCES", + 0xC00000B0: "STATUS_PIPE_DISCONNECTED", 0xC00000BA: "STATUS_FILE_IS_A_DIRECTORY", 0xC00000BB: "STATUS_NOT_SUPPORTED", 0xC00000C9: "STATUS_NETWORK_NAME_DELETED", @@ -73,5 +74,7 @@ 0xC000020C: "STATUS_CONNECTION_DISCONNECTED", 0xC0000225: "STATUS_NOT_FOUND", 0xC0000257: "STATUS_PATH_NOT_COVERED", + 0xC000026E: "STATUS_VOLUME_DISMOUNTED", + 0xC00002FB: "STATUS_KDC_INVALID_REQUEST", 0xC000035C: "STATUS_NETWORK_SESSION_EXPIRED", } diff --git a/scapy/layers/windows/registry.py b/scapy/layers/windows/registry.py index 640f09829cd..62c26216c0c 100644 --- a/scapy/layers/windows/registry.py +++ b/scapy/layers/windows/registry.py @@ -105,6 +105,7 @@ class RegType(IntEnum): # These constants are used to specify the type of a registry value. + REG_NONE = 0 # No defined value type REG_SZ = 1 # Unicode string REG_EXPAND_SZ = 2 # Unicode string with environment variable expansion REG_BINARY = 3 # Binary data @@ -194,7 +195,7 @@ def __init__( ]: if not isinstance(reg_data, str): raise ValueError("Data must be a 'str' for this type.") - elif reg_type == RegType.REG_BINARY: + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: if not isinstance(reg_data, bytes): raise ValueError("Data must be a 'bytes' for this type.") elif reg_type in [ @@ -227,7 +228,7 @@ def encode(self) -> bytes: RegType.REG_LINK, ]: return self.reg_data.encode("utf-16le") - elif self.reg_type == RegType.REG_BINARY: + elif self.reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: return self.reg_data elif self.reg_type in [ RegType.REG_DWORD, @@ -257,7 +258,7 @@ def frombytes(reg_name: str, reg_type: RegType, data: bytes): RegType.REG_LINK, ]: reg_data = data.decode("utf-16le") - elif reg_type == RegType.REG_BINARY: + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: reg_data = data elif reg_type in [ RegType.REG_DWORD, @@ -292,7 +293,7 @@ def fromstr(reg_name: str, reg_type: RegType, data: str): RegType.REG_LINK, ]: reg_data = data - elif reg_type == RegType.REG_BINARY: + elif reg_type in [RegType.REG_NONE, RegType.REG_BINARY]: reg_data = bytes.fromhex(data) elif reg_type in [ RegType.REG_DWORD, @@ -493,6 +494,26 @@ def get_key_info( timeout=timeout, ) + if response.status != 0: + log_runtime.error( + "Got status %s while querying key info", hex(response.status) + ) + raise ValueError(response.status) + + if response.lpClassOut.Length > 2: + # There is a Class info stored. We need to + # get it by specifying the proper MaximumLength. + # By default the size is "2". + response = self.sr1_req( + BaseRegQueryInfoKey_Request( + hKey=key_handle, + lpClassIn=RPC_UNICODE_STRING( + MaximumLength=response.lpClassOut.Length + ), + ), + timeout=timeout, + ) + if response.status != 0: log_runtime.error( "Got status %s while querying key info", hex(response.status) @@ -588,7 +609,6 @@ def enum_subkeys( index += 1 results.append(response.lpNameOut.valueof("Buffer")[:-1].decode()) - return results def enum_values( diff --git a/scapy/libs/codec.py b/scapy/libs/codec.py new file mode 100644 index 00000000000..bcffbec3508 --- /dev/null +++ b/scapy/libs/codec.py @@ -0,0 +1,362 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Generic codec base classes combining identical parts from the ASN.1 and +CBOR codec implementations. + +Both the ASN.1 BER codec (BERcodec_Object) and the CBOR codec +(CBORcodec_Object) share: + +- A metaclass that registers each codec class with its associated tag upon + class creation (``GenericCodec_metaclass``). +- A base codec class providing ``check_string``, ``dec``, ``safedec``, and + ``enc`` template methods (``GenericCodecObject``). + +Both the ASN.1 field layer (ASN1F_field / ASN1F_optional) and the CBOR +field layer (CBORF_field / CBORF_optional) share a large set of utility +methods that are identical across formats: + +- ``GenericCodecField_element`` — empty marker base (replaces per-format + ``ASN1F_element`` / ``CBORF_element`` as a common ancestor). +- ``GenericCodecField[_I, _A]`` — provides the shared field utility methods: + ``register_owner``, ``i2repr``, ``i2h``, ``any2i``, ``extract_packet``, + ``build``, ``dissect``, ``do_copy``, ``set_val``, ``is_empty``, + ``get_fields_list``, ``__str__``, and ``copy``. + The only format-specific hook is ``_badsequence_error_class``. +- ``GenericCodecOptionalField`` — provides the shared optional-wrapper + methods: ``__getattr__``, ``m2i`` / ``dissect`` (parameterised by + ``_optional_error_classes``), ``build``, ``any2i``, and ``i2repr``. +""" + +import copy as _copy + +from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar, cast + +_K = TypeVar('_K') +_I = TypeVar('_I') +_A = TypeVar('_A') + + +class GenericCodec_metaclass(type): + """Metaclass for codec objects shared by BER and CBOR implementations. + + Upon class creation, registers each codec class with its associated tag + by calling ``c.tag.register(c.codec, c)``. Subclass metaclasses can + customise the behaviour on registration failure by overriding + ``_handle_registration_error``. + """ + + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Any + ): + # type: (...) -> Type[GenericCodecObject[Any]] + c = cast( + 'Type[GenericCodecObject[Any]]', + super(GenericCodec_metaclass, cls).__new__(cls, name, bases, dct) + ) + try: + c.tag.register(c.codec, c) + except Exception as exc: + cls._handle_registration_error(c, exc) + return c + + @classmethod + def _handle_registration_error(cls, c, exc): + # type: (Type[Any], Exception) -> None + """Called when tag registration fails. Override to add logging.""" + pass + + +class GenericCodecObject(Generic[_K], metaclass=GenericCodec_metaclass): + """Generic base class for codec objects. + + Combines the identical functionality shared between ASN.1's + ``BERcodec_Object`` and CBOR's ``CBORcodec_Object``: + + * ``check_string`` — raises a decoding error when the input is empty. + * ``dec`` — decodes bytes with optional *safe* mode that wraps errors in + an error object instead of raising an exception. + * ``safedec`` — convenience wrapper that calls ``dec`` in safe mode. + * ``enc`` — encode stub (must be implemented by concrete subclasses). + + Concrete subclasses must define the following **class-level** attributes + so that the shared methods work correctly: + + ``tag`` + The codec tag (e.g. an ``ASN1Tag`` or ``CBORTag`` instance). + ``codec`` + The codec identifier (e.g. ``ASN1_Codecs.BER`` or + ``CBOR_Codecs.CBOR``). + ``_decoding_error_class`` + Exception class instantiated by ``check_string`` when the input is + empty (e.g. ``BER_Decoding_Error`` or ``CBOR_Codec_Decoding_Error``). + ``_generic_error_classes`` + Tuple of exception classes caught by ``dec`` when operating in safe + mode (e.g. ``(BER_Decoding_Error, ASN1_Error)``). + ``_decoding_error_object_class`` + Object class used to wrap decoding errors in safe mode (e.g. + ``ASN1_DECODING_ERROR`` or ``CBOR_DECODING_ERROR``). + + Concrete subclasses must also implement: + + ``do_dec(cls, s, context, safe)`` + The actual decoding logic. + ``enc(cls, s)`` + The encoding logic. + """ + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + """Raise a decoding error if the input bytes *s* are empty.""" + if not s: + raise cls._decoding_error_class( # type: ignore + "%s: Got empty object while expecting tag %r" % + (cls.__name__, cls.tag), + remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False # type: bool + ): + # type: (...) -> Tuple[Any, bytes] + """Decode bytes. + + Raises :exc:`NotImplementedError` by default; concrete subclasses must + override this method with format-specific decode logic. + """ + raise NotImplementedError("Subclasses must implement do_dec") + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Any] + safe=False # type: bool + ): + # type: (...) -> Tuple[Any, bytes] + """Decode bytes with optional *safe* mode. + + When *safe* is ``False`` (the default), any decoding exception + propagates to the caller unchanged. + + When *safe* is ``True``, exceptions listed in + ``_generic_error_classes`` are caught and returned as an instance of + ``_decoding_error_object_class`` paired with an empty remainder + (``b""``), so callers never receive an exception in safe mode. + """ + if not safe: + return cls.do_dec(s, context, safe) + try: + return cls.do_dec(s, context, safe) + except cls._generic_error_classes as e: # type: ignore + return cls._decoding_error_object_class(s, exc=e), b"" # type: ignore + + @classmethod + def safedec(cls, + s, # type: bytes + context=None # type: Optional[Any] + ): + # type: (...) -> Tuple[Any, bytes] + """Decode bytes in safe mode (decoding errors are wrapped, not raised). + + This is a convenience wrapper around ``dec(s, context, safe=True)``. + """ + return cls.dec(s, context, safe=True) + + @classmethod + def enc(cls, s): + # type: (Any) -> bytes + """Encode *s* to bytes. Must be implemented by concrete subclasses.""" + raise NotImplementedError("Subclasses must implement enc") + + +############################################## +# Generic codec field base classes # +############################################## + + +class GenericCodecField_element(object): + """Marker base class for all codec field elements. + + Both ``ASN1F_element`` (ASN.1) and ``CBORF_element`` (CBOR) inherit from + this class so that format-agnostic code can test ``isinstance(obj, + GenericCodecField_element)`` without importing format-specific symbols. + """ + pass + + +class GenericCodecField(Generic[_I, _A]): + """Shared utility methods for codec packet fields. + + ``ASN1F_field`` and ``CBORF_field`` are both structured as format-specific + thin layers on top of this base. All methods listed here are byte-for-byte + identical in the two implementations; only the exception class used in + ``extract_packet`` differs and is injected via ``_badsequence_error_class``. + + Concrete subclasses **must** define: + + ``name`` (str) + Field name — set by the subclass ``__init__``. + ``owners`` (list) + List of packet classes that own this field — set by ``__init__``. + ``_badsequence_error_class`` (exception class) + Exception caught in ``extract_packet`` when the nested packet cannot + be parsed (e.g. ``ASN1F_badsequence`` or ``CBORF_badsequence``). + + Concrete subclasses **must** also implement: + + ``m2i(self, pkt, s)`` + Format-specific machine-to-internal conversion. + ``i2m(self, pkt, x)`` + Format-specific internal-to-machine conversion. + ``randval(self)`` + Return a random value generator appropriate for the field type. + """ + + holds_packets = 0 + islist = 0 + _badsequence_error_class = Exception # type: Type[Exception] + + def register_owner(self, cls): + # type: (Any) -> None + self.owners.append(cls) # type: ignore[attr-defined] + + def i2repr(self, pkt, x): + # type: (Any, Any) -> str + return repr(x) + + def i2h(self, pkt, x): + # type: (Any, Any) -> Any + return x + + def any2i(self, pkt, x): + # type: (Any, Any) -> _I + return cast(_I, x) + + def extract_packet(self, + cls, # type: Any + s, # type: bytes + _underlayer=None, # type: Optional[Any] + ): + # type: (...) -> Tuple[Any, bytes] + """Extract a nested packet from bytes *s*. + + On success returns ``(packet, remainder)``. If ``cls(s, ...)`` raises + ``_badsequence_error_class``, falls back to a ``Raw`` packet so that + the caller always receives a packet object. + """ + from scapy import packet as _packet + try: + c = cls(s, _underlayer=_underlayer) + except self._badsequence_error_class: + c = _packet.Raw(s, _underlayer=_underlayer) # type: ignore[assignment] + cpad = c.getlayer(_packet.Raw) + s = b"" + if cpad is not None: + s = cpad.load + if cpad.underlayer: + del cpad.underlayer.payload + return c, s + + def build(self, pkt): + # type: (Any) -> bytes + return self.i2m(pkt, getattr(pkt, self.name)) # type: ignore[attr-defined] + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + v, s = self.m2i(pkt, s) # type: ignore[attr-defined] + self.set_val(pkt, v) + return s + + def do_copy(self, x): + # type: (Any) -> Any + from scapy.base_classes import BasePacket + if isinstance(x, list): + x = x[:] + for i in range(len(x)): + if isinstance(x[i], BasePacket): + x[i] = x[i].copy() + return x + if hasattr(x, "copy"): + return x.copy() + return x + + def set_val(self, pkt, val): + # type: (Any, Any) -> None + setattr(pkt, self.name, val) # type: ignore[attr-defined] + + def is_empty(self, pkt): + # type: (Any) -> bool + return getattr(pkt, self.name) is None # type: ignore[attr-defined] + + def get_fields_list(self): + # type: () -> List[Any] + return [self] + + def __str__(self): + # type: () -> str + return repr(self) + + def copy(self): + # type: () -> Any + return _copy.copy(self) + + +class GenericCodecOptionalField(object): + """Shared optional-wrapper logic for ``ASN1F_optional`` and + ``CBORF_optional``. + + Both wrappers delegate all work to a wrapped ``_field`` object. The only + format-specific part is the set of exception classes caught in ``m2i`` and + ``dissect``, injected via ``_optional_error_classes``. + + Concrete subclasses **must** define: + + ``_field`` + The wrapped field object — set by the subclass ``__init__``. + ``_optional_error_classes`` (tuple of exception classes) + Exceptions caught during optional decoding (e.g. + ``(ASN1_Error, ASN1F_badsequence, BER_Decoding_Error)``). + """ + + _optional_error_classes = (Exception,) # type: Tuple[Type[Exception], ...] + + def __getattr__(self, attr): + # type: (str) -> Any + return getattr(self._field, attr) # type: ignore[attr-defined] + + def m2i(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + try: + return self._field.m2i(pkt, s) # type: ignore[attr-defined] + except self._optional_error_classes: + return None, s + + def dissect(self, pkt, s): + # type: (Any, bytes) -> bytes + try: + return self._field.dissect(pkt, s) # type: ignore[attr-defined] + except self._optional_error_classes: + self._field.set_val(pkt, None) # type: ignore[attr-defined] + return s + + def build(self, pkt): + # type: (Any) -> bytes + if self._field.is_empty(pkt): # type: ignore[attr-defined] + return b"" + return self._field.build(pkt) # type: ignore[attr-defined] + + def any2i(self, pkt, x): + # type: (Any, Any) -> Any + return self._field.any2i(pkt, x) # type: ignore[attr-defined] + + def i2repr(self, pkt, x): + # type: (Any, Any) -> str + return self._field.i2repr(pkt, x) # type: ignore[attr-defined] diff --git a/scapy/modules/ldaphero.py b/scapy/modules/ldaphero.py index 1a43e55ec66..cd4bd4fb658 100644 --- a/scapy/modules/ldaphero.py +++ b/scapy/modules/ldaphero.py @@ -144,6 +144,10 @@ class LDAPHero: :param HashNt: if provided, used for auth (NTLM) :param HashAes256Sha96: if provided, used for auth (Kerberos) :param HashAes128Sha96: if provided, used for auth (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. + :param use_winssp: (bool) (only works on Windows). Use implicit authentication + through WinSSP. """ def __init__( @@ -163,9 +167,14 @@ def __init__( HashAes256Sha96: bytes = None, HashAes128Sha96: bytes = None, use_krb5ccname: bool = False, + use_winssp: bool = False, ): self.client = LDAP_Client() - if ssp is None and mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO and UPN and host: + if ( + ssp is None + and mech == LDAP_BIND_MECHS.SASL_GSS_SPNEGO + and (UPN and host or use_winssp) + ): # We allow the SSP to be provided through arguments. # In that case, use SPNEGO ssp = SPNEGOSSP.from_cli_arguments( @@ -177,6 +186,7 @@ def __init__( HashAes128Sha96=HashAes128Sha96, kerberos_required=kerberos_required, use_krb5ccname=use_krb5ccname, + use_winssp=use_winssp, ) self.ssp = ssp self.mech = mech @@ -261,6 +271,7 @@ def connect(self): self.client.connect(self.host, port=self.port, use_ssl=self.ssl) except Exception as ex: self.tprint(str(ex)) + self.host = None raise self.tprint("Established connection to %s." % self.host) self.connected = True diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 16716ae0637..4bc469a78f9 100644 --- a/scapy/modules/ticketer.py +++ b/scapy/modules/ticketer.py @@ -2594,7 +2594,7 @@ def resign_ticket(self, i, hash=None, kdc_hash=None): def request_tgt( self, - upn, + upn=None, ip=None, key=None, password=None, diff --git a/scapy/packet.py b/scapy/packet.py index 3fb2eac6d92..d2bf6f67bfa 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -222,6 +222,7 @@ def __init__(self, Optional[int], Optional[bytes], ] + _PickleStateType = Union[_PickleType, Dict[str, Any]] @property def comment(self): @@ -243,27 +244,68 @@ def comment(self, value): else: self.comments = None + @classmethod + def _rebuild_pkt(cls, raw_packet): + # type: (Type[Packet], bytes) -> Packet + """Helper used by pickle to reconstruct Packet from raw bytes.""" + return cls(raw_packet) + def __reduce__(self): - # type: () -> Tuple[Type[Packet], Tuple[bytes], Packet._PickleType] + # type: () -> Tuple[Any, ...] """Used by pickling methods""" - return (self.__class__, (self.build(),), ( - self.time, - self.sent_time, - self.direction, - self.sniffed_on, - self.wirelen, - self.comment - )) + state = { + "pickle_state_version": 2, + "time": self.time, + "sent_time": self.sent_time, + "direction": self.direction, + "sniffed_on": self.sniffed_on, + "wirelen": self.wirelen, + # Keep both keys for compatibility with historical/transition code. + "comment": self.comment, + "comments": self.comments, + } + extra_slots = {} + for attr in type(self).__all_slots__ - set(Packet.__slots__): + if hasattr(self, attr): + extra_slots[attr] = getattr(self, attr) + if extra_slots: + state["extra_slots"] = extra_slots # type: ignore + return (type(self)._rebuild_pkt, (self.build(),), state) def __setstate__(self, state): - # type: (Packet._PickleType) -> Packet + # type: (Packet._PickleStateType) -> Packet """Rebuild state using pickable methods""" - self.time = state[0] - self.sent_time = state[1] - self.direction = state[2] - self.sniffed_on = state[3] - self.wirelen = state[4] - self.comment = state[5] + # Legacy format: tuple produced by older Packet.__reduce__. + if isinstance(state, tuple): + self.time = state[0] + self.sent_time = state[1] + self.direction = state[2] + self.sniffed_on = state[3] + self.wirelen = state[4] + self.comment = state[5] + return self + + # New format: versioned dict metadata. + self.time = state.get("time", self.time) + self.sent_time = state.get("sent_time", self.sent_time) + self.direction = state.get("direction", self.direction) + self.sniffed_on = state.get("sniffed_on", self.sniffed_on) + self.wirelen = state.get("wirelen", self.wirelen) + + if "comments" in state: + self.comments = state["comments"] + elif "comment" in state: + self.comment = state["comment"] + + extra_slots = state.get("extra_slots", {}) + if isinstance(extra_slots, dict): + for attr, value in extra_slots.items(): + # Only restore known subclass slots; ignore stale/unknown entries. + if attr in type(self).__all_slots__ and attr not in Packet.__slots__: + try: + setattr(self, attr, value) + except AttributeError: + pass return self def __deepcopy__(self, diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 3b119a1e905..2ab413eb2c7 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -133,7 +133,8 @@ def __init__(self, threaded=True, # type: bool session=None, # type: Optional[_GlobSessionType] chainEX=False, # type: bool - stop_filter=None # type: Optional[Callable[[Packet], bool]] + stop_filter=None, # type: Optional[Callable[[Packet], bool]] + **send_kwargs, # type: Any ): # type: (...) -> None # Instantiate all arguments @@ -162,6 +163,7 @@ def __init__(self, self._flood = _flood self.threaded = threaded self.breakout = Event() + self.send_kwargs = send_kwargs # Instantiate packet holders if prebuild and not self._flood: self.tobesent = list(pkt) # type: _PacketIterable @@ -278,7 +280,7 @@ def _sndrcv_snd(self): # has not been sent self.hsent.setdefault(p.hashret(), []).append(p) # Send packet - self.pks.send(p) + self.pks.send(p, **self.send_kwargs) time.sleep(self.inter) if self.breakout.is_set(): break diff --git a/scapy/tools/UTscapy.py b/scapy/tools/UTscapy.py index 119775b0258..f724a845fc0 100644 --- a/scapy/tools/UTscapy.py +++ b/scapy/tools/UTscapy.py @@ -523,8 +523,8 @@ def _run_test_timeout(test, get_interactive_session, verb=3, my_globals=None): timeout=5 * 60, # 5 min verb=verb, my_globals=my_globals) - except StopAutorunTimeout: - return "-- Test timed out ! --", False + except StopAutorunTimeout as ex: + return "@@@@@@@@@@@@@@@@@ Test timed out ! @@@@@@@@@@@@@@@@@\n" + ex.code_run, False def run_test(test, get_interactive_session, theme, verb=3, @@ -984,6 +984,7 @@ def main(): FORMAT = Format.ANSI OUTPUTFILE = sys.stdout + OUTPUTFILE.reconfigure(encoding='utf-8') LOCAL = 0 NUM = None NON_ROOT = False diff --git a/scapy/utils.py b/scapy/utils.py index 2aea911462d..09b0fbe97ae 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -3926,8 +3926,12 @@ def loop(self, debug: int = 0) -> None: for args in calls: try: res = func(self, *args, **kwargs) - except TypeError: + except KeyboardInterrupt: + print("Aborted.") + except TypeError as ex: print("Bad number of arguments !") + if debug: + traceback.print_exception(ex) self.help(cmd=cmd) continue except Exception as ex: @@ -3937,6 +3941,8 @@ def loop(self, debug: int = 0) -> None: try: if res and cmd in self.commands_output: self.commands_output[cmd](self, res, **outkwargs) + except KeyboardInterrupt: + print("Aborted.") except Exception as ex: print("Output processor failed with error: %s" % ex) @@ -4016,13 +4022,13 @@ def AutoArgparse( continue if param.default != inspect.Parameter.empty: if param.kind == inspect.Parameter.POSITIONAL_ONLY: - positional.append(param.name) + positional.append(parname) paramkwargs["nargs"] = '?' else: parname = "--" + parname paramkwargs["default"] = param.default else: - positional.append(param.name) + positional.append(parname) if param.kind == inspect.Parameter.VAR_POSITIONAL: paramkwargs["action"] = "append" if param.name in argsdoc: diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index 722b2bc974d..9b8d0a175aa 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -612,4 +612,128 @@ log = get_log(pkt) print(log) assert len(log) == 2 assert log[1] == "0x80" -assert log[0] == "DeviceControlPositiveResponse" \ No newline at end of file +assert log[0] == "DeviceControlPositiveResponse" ++ Single layer GMLAN mode + += Single layer mode: enable and basic dissect + +conf.contribs['GMLAN']['single_layer_mode'] = True + +ido = GMLAN(b'\x10\x02') +assert isinstance(ido, GMLAN_IDO), "Expected GMLAN_IDO, got %s" % type(ido) +assert ido.service == 0x10 +assert ido.subfunction == 0x02 + += Single layer mode: build GMLAN_IDO + +ido_built = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_built) == b'\x10\x02', "Expected b'\\x10\\x02', got %s" % bytes(ido_built).hex() + += Single layer mode: dissect positive response (using SA which has a PR class) + +sapr = GMLAN(b'\x67\x01\xde\xad') +assert isinstance(sapr, GMLAN_SAPR), "Expected GMLAN_SAPR, got %s" % type(sapr) +assert sapr.service == 0x67 +assert sapr.subfunction == 0x01 + += Single layer mode: NegativeResponse dissect + +nr = GMLAN(b'\x7f\x10\x22') +assert isinstance(nr, GMLAN_NR), "Expected GMLAN_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.returnCode == 0x22 + += Single layer mode: NegativeResponse answers() + +ido2 = GMLAN_IDO(subfunction=0x02) +nr2 = GMLAN_NR(requestServiceId=0x10, returnCode=0x22) +assert nr2.answers(ido2) + += Single layer mode: hashret consistency between request and positive response (SA) + +sa3 = GMLAN_SA(subfunction=0x01) +sapr3 = GMLAN_SAPR(subfunction=0x01) +assert sa3.hashret() == sapr3.hashret(), \ + "hashret mismatch: %s vs %s" % (sa3.hashret().hex(), sapr3.hashret().hex()) + += Single layer mode: sub-subpacket bindings are unaffected + +rfrdpr = GMLAN(b'\x52\x01\x00\x01\x02\x03\x04') +assert isinstance(rfrdpr, GMLAN_RFRDPR), "Expected GMLAN_RFRDPR, got %s" % type(rfrdpr) + += Single layer mode: unknown service falls back to GMLAN + +unknown = GMLAN(b'\xBB\x01\x02') +assert isinstance(unknown, GMLAN), "Expected GMLAN fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['GMLAN']['single_layer_mode'] = False + +ido4 = GMLAN(b'\x10\x02') +assert ido4.__class__ == GMLAN +assert ido4.service == 0x10 +assert ido4[GMLAN_IDO].subfunction == 0x02 + += Single layer mode: cleanup + +conf.contribs['GMLAN']['single_layer_mode'] = False +assert not conf.contribs['GMLAN']['single_layer_mode'] + ++ Compatibility mode GMLAN + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['GMLAN']['single_layer_mode'] = True +conf.contribs['GMLAN']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +ido_sa = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_sa) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte, got %s" % bytes(ido_sa).hex() +assert ido_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked) == b'\x10\x02', \ + "Stacked GMLAN/GMLAN_IDO should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +ido_dis = GMLAN(b'\x10\x02') +assert isinstance(ido_dis, GMLAN_IDO) +assert ido_dis.service == 0x10 +assert ido_dis.subfunction == 0x02 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['GMLAN']['compatibility_mode'] = False + +stacked_nc = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_nc) == b'\x10\x10\x02', \ + "With compat OFF, stacked GMLAN/GMLAN_IDO should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +ido_nc = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_nc) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte even with compat OFF, got %s" % bytes(ido_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = False + +stacked_slm_off = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_slm_off) == b'\x10\x02', \ + "With SLM OFF, no service field in GMLAN_IDO regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = True +assert not conf.contribs['GMLAN']['single_layer_mode'] +assert conf.contribs['GMLAN']['compatibility_mode'] diff --git a/test/contrib/automotive/kwp.uts b/test/contrib/automotive/kwp.uts index d525b8554e6..73c99dff1a2 100644 --- a/test/contrib/automotive/kwp.uts +++ b/test/contrib/automotive/kwp.uts @@ -507,3 +507,144 @@ nrc = KWP(b'\x7f\x22\x33') assert nrc.service == 0x7f assert nrc.requestServiceId == 0x22 assert nrc.negativeResponseCode == 0x33 + ++ Single layer KWP mode + += Single layer mode: enable and basic dissect + +conf.contribs['KWP']['single_layer_mode'] = True + +sds = KWP(b'\x10\x01') +assert isinstance(sds, KWP_SDS), "Expected KWP_SDS, got %s" % type(sds) +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Single layer mode: build KWP_SDS + +sds_built = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(sds_built).hex() + += Single layer mode: dissect positive response + +sdspr = KWP(b'\x50\x01\xbe\xef') +assert isinstance(sdspr, KWP_SDSPR), "Expected KWP_SDSPR, got %s" % type(sdspr) +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x01 + += Single layer mode: answers() between subpackets + +sds2 = KWP_SDS(diagnosticSession=0x01) +sdspr2 = KWP_SDSPR(diagnosticSession=0x01) +assert sdspr2.answers(sds2) + += Single layer mode: NegativeResponse dissect + +nr = KWP(b'\x7f\x10\x22') +assert isinstance(nr, KWP_NR), "Expected KWP_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +sds3 = KWP_SDS(diagnosticSession=0x01) +nr2 = KWP_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(sds3) + += Single layer mode: hashret consistency between request and positive response + +sds4 = KWP_SDS(diagnosticSession=0x01) +sdspr4 = KWP_SDSPR(diagnosticSession=0x01) +assert sds4.hashret() == sdspr4.hashret(), \ + "hashret mismatch: %s vs %s" % (sds4.hashret().hex(), sdspr4.hashret().hex()) + += Single layer mode: unknown service falls back to KWP + +unknown = KWP(b'\xAA\x01\x02') +assert isinstance(unknown, KWP), "Expected KWP fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['KWP']['single_layer_mode'] = False + +sds5 = KWP(b'\x10\x01') +assert sds5.__class__ == KWP +assert sds5.service == 0x10 +assert sds5[KWP_SDS].diagnosticSession == 0x01 + += Single layer mode: idempotency + +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['single_layer_mode'] = True +sds6 = KWP(b'\x10\x01') +assert isinstance(sds6, KWP_SDS) + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['single_layer_mode'] = False +sds7 = KWP(b'\x10\x01') +assert sds7.__class__ == KWP +count = sum(1 for fval, cls in KWP.payload_guess + if fval.get('service') == 0x10 and cls == KWP_SDS) +assert count == 1, "Expected 1 binding for KWP_SDS, got %d" % count + += Single layer mode: cleanup + +conf.contribs['KWP']['single_layer_mode'] = False +assert not conf.contribs['KWP']['single_layer_mode'] + ++ Compatibility mode KWP + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +sds_sa = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_sa) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte, got %s" % bytes(sds_sa).hex() +assert sds_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked KWP/KWP_SDS should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +sds_dis = KWP(b'\x10\x01') +assert isinstance(sds_dis, KWP_SDS) +assert sds_dis.service == 0x10 +assert sds_dis.diagnosticSession == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_nc = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked KWP/KWP_SDS should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +sds_nc = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_nc) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte even with compat OFF, got %s" % bytes(sds_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_slm_off = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in KWP_SDS regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = True +assert not conf.contribs['KWP']['single_layer_mode'] +assert conf.contribs['KWP']['compatibility_mode'] diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index fa65e95e447..2c5e8e71e11 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -1031,3 +1031,127 @@ assert b[22:] == b'ABCDEFGHIJKLMNOP' r = OBD(b'\x09\x02\x04') assert p.answers(r) + ++ Single layer OBD mode + += Single layer mode: enable and basic dissect + +conf.contribs['OBD']['single_layer_mode'] = True + +s01 = OBD(b'\x01\x0c') +assert isinstance(s01, OBD_S01), "Expected OBD_S01, got %s" % type(s01) +assert s01.service == 0x01 + += Single layer mode: build OBD_S01 + +s01_built = OBD_S01(pid=[0x0c]) +assert bytes(s01_built) == b'\x01\x0c', "Expected b'\\x01\\x0c', got %s" % bytes(s01_built).hex() + += Single layer mode: dissect positive response + +s01pr = OBD(b'\x41\x0c\x0f\xa0') +assert isinstance(s01pr, OBD_S01_PR), "Expected OBD_S01_PR, got %s" % type(s01pr) +assert s01pr.service == 0x41 + += Single layer mode: NegativeResponse dissect + +nr = OBD(b'\x7f\x01\x22') +assert isinstance(nr, OBD_NR), "Expected OBD_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.request_service_id == 0x01 +assert nr.response_code == 0x22 + += Single layer mode: NegativeResponse answers() + +s01_2 = OBD_S01(pid=[0x0c]) +nr2 = OBD_NR(request_service_id=0x01, response_code=0x22) +assert nr2.answers(s01_2) + += Single layer mode: hashret consistency between request and positive response + +s09 = OBD_S09(iid=[0x02]) +s09pr = OBD_S09_PR() +assert s09.hashret() == s09pr.hashret(), \ + "hashret mismatch: %s vs %s" % (s09.hashret().hex(), s09pr.hashret().hex()) + += Single layer mode: unknown service falls back to OBD + +unknown = OBD(b'\xBB\x01\x02') +assert isinstance(unknown, OBD), "Expected OBD fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['OBD']['single_layer_mode'] = False + +s01_3 = OBD(b'\x01\x0c') +assert s01_3.__class__ == OBD +assert s01_3.service == 0x01 +assert isinstance(s01_3[OBD_S01], OBD_S01) + += Single layer mode: cleanup + +conf.contribs['OBD']['single_layer_mode'] = False +assert not conf.contribs['OBD']['single_layer_mode'] + ++ Compatibility mode OBD + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['OBD']['single_layer_mode'] = True +conf.contribs['OBD']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +s01_sa = OBD_S01(pid=[0x0c]) +assert bytes(s01_sa)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte 0x01, got %s" % bytes(s01_sa).hex() + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = OBD() / OBD_S01(pid=[0x0c]) +stacked_bytes = bytes(stacked) +assert stacked_bytes[0:1] == b'\x01', \ + "Stacked OBD/OBD_S01 first byte should be 0x01 (OBD service), got %s" % stacked_bytes.hex() +assert stacked_bytes[1:2] != b'\x01', \ + "No duplicate service byte expected, got %s" % stacked_bytes.hex() +assert len(stacked_bytes) == 2, \ + "Stacked OBD/OBD_S01(pid=[0x0c]) should be 2 bytes, got %s" % stacked_bytes.hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +s01_dis = OBD(b'\x01\x0c') +assert isinstance(s01_dis, OBD_S01) + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_nc = OBD() / OBD_S01(pid=[0x0c]) +stacked_nc_bytes = bytes(stacked_nc) +assert len(stacked_nc_bytes) == 3, \ + "With compat OFF, stacked OBD/OBD_S01 should produce 3 bytes (duplicate service), got %s" % stacked_nc_bytes.hex() +assert stacked_nc_bytes[0:1] == b'\x01' and stacked_nc_bytes[1:2] == b'\x01', \ + "With compat OFF, first two bytes should both be service 0x01, got %s" % stacked_nc_bytes.hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +s01_nc = OBD_S01(pid=[0x0c]) +assert bytes(s01_nc)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte even with compat OFF" + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_slm_off = OBD() / OBD_S01(pid=[0x0c]) +slm_off_bytes = bytes(stacked_slm_off) +assert len(slm_off_bytes) == 2, \ + "With SLM OFF, no service field in OBD_S01 regardless of compat mode, got %s" % slm_off_bytes.hex() + += Compatibility mode: cleanup + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = True +assert not conf.contribs['OBD']['single_layer_mode'] +assert conf.contribs['OBD']['compatibility_mode'] diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 1545076039d..e4a6bf07ba8 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1438,3 +1438,179 @@ nrc = UDS(b'\x7f\x22\x33') assert nrc.service == 0x7f assert nrc.requestServiceId == 0x22 assert nrc.negativeResponseCode == 0x33 + ++ Single layer UDS mode + += Single layer mode: enable and basic dissect + +conf.contribs['UDS']['single_layer_mode'] = True + +dsc = UDS(b'\x10\x01') +assert isinstance(dsc, UDS_DSC), "Expected UDS_DSC, got %s" % type(dsc) +assert dsc.service == 0x10 +assert dsc.diagnosticSessionType == 0x01 + += Single layer mode: build UDS_DSC + +dsc_built = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(dsc_built).hex() + += Single layer mode: UDS() / UDS_DSC() still works in single layer mode + +dsc_two = UDS_DSC(service=0x10, diagnosticSessionType=0x01) +assert dsc_two.service == 0x10 +assert dsc_two.diagnosticSessionType == 0x01 + += Single layer mode: dissect positive response + +dscpr = UDS(b'\x50\x01beef') +assert isinstance(dscpr, UDS_DSCPR), "Expected UDS_DSCPR, got %s" % type(dscpr) +assert dscpr.service == 0x50 +assert dscpr.diagnosticSessionType == 0x01 +assert dscpr.sessionParameterRecord == b"beef" + += Single layer mode: answers() between subpackets + +dsc = UDS_DSC(diagnosticSessionType=0x01) +dscpr = UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"beef") +assert dscpr.answers(dsc) + += Single layer mode: answers() negative (different session type) + +dsc2 = UDS_DSC(diagnosticSessionType=0x02) +dscpr2 = UDS_DSCPR(diagnosticSessionType=0x01) +assert not dscpr2.answers(dsc2) + += Single layer mode: NegativeResponse dissect + +nr = UDS(b'\x7f\x10\x22') +assert isinstance(nr, UDS_NR), "Expected UDS_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +dsc3 = UDS_DSC(diagnosticSessionType=0x01) +nr2 = UDS_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(dsc3) + += Single layer mode: NegativeResponse does not answer wrong service + +er = UDS_ER(resetType=0x01) +assert not nr2.answers(er) + += Single layer mode: hashret consistency between request and positive response + +dsc4 = UDS_DSC(diagnosticSessionType=0x01) +dscpr4 = UDS_DSCPR(diagnosticSessionType=0x01) +assert dsc4.hashret() == dscpr4.hashret(), \ + "hashret mismatch: %s vs %s" % (dsc4.hashret().hex(), dscpr4.hashret().hex()) + += Single layer mode: UDS_RDBI dissect + +rdbi = UDS(b'\x22\x01\x02\x03\x04') +assert isinstance(rdbi, UDS_RDBI), "Expected UDS_RDBI, got %s" % type(rdbi) +assert rdbi.service == 0x22 + += Single layer mode: unknown service falls back to UDS + +unknown = UDS(b'\xAA\x01\x02') +assert isinstance(unknown, UDS), "Expected UDS fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +conf.contribs['UDS']['single_layer_mode'] = False + +dsc5 = UDS(b'\x10\x01') +assert dsc5.__class__ == UDS +assert dsc5.service == 0x10 +assert dsc5[UDS_DSC].diagnosticSessionType == 0x01 + +dscpr5 = UDS(b'\x50\x01beef') +assert dscpr5.__class__ == UDS +assert dscpr5.service == 0x50 +assert dscpr5[UDS_DSCPR].diagnosticSessionType == 0x01 + += Single layer mode: enable via conf directly + +conf.contribs['UDS']['single_layer_mode'] = True + +er6 = UDS(b'\x11\x01') +assert isinstance(er6, UDS_ER), "Expected UDS_ER, got %s" % type(er6) +assert er6.service == 0x11 +assert er6.resetType == 0x01 + += Single layer mode: final cleanup - restore default multi-layer mode + +conf.contribs['UDS']['single_layer_mode'] = False +assert not conf.contribs['UDS']['single_layer_mode'] + ++ Compatibility mode UDS + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['UDS']['single_layer_mode'] = True +conf.contribs['UDS']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +dsc_sa = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_sa) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte, got %s" % bytes(dsc_sa).hex() +assert dsc_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked UDS/UDS_DSC should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: positive response stacked suppresses service + +stacked_pr = UDS() / UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"") +assert bytes(stacked_pr) == b'\x50\x01', \ + "Stacked UDS/UDS_DSCPR should produce 2 bytes, got %s" % bytes(stacked_pr).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +dsc_dis = UDS(b'\x10\x01') +assert isinstance(dsc_dis, UDS_DSC) +assert dsc_dis.service == 0x10 +assert dsc_dis.diagnosticSessionType == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['UDS']['compatibility_mode'] = False + +stacked_nc = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked UDS/UDS_DSC should produce 3 bytes (duplicate service), got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +dsc_nc = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_nc) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte even with compat OFF, got %s" % bytes(dsc_nc).hex() + += Compatibility mode OFF + SLM ON: dissect standalone still works + +dsc_dis2 = UDS(b'\x10\x01') +assert isinstance(dsc_dis2, UDS_DSC) +assert dsc_dis2.service == 0x10 + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = False + +stacked_slm_off = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in UDS_DSC regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = True +assert not conf.contribs['UDS']['single_layer_mode'] +assert conf.contribs['UDS']['compatibility_mode'] diff --git a/test/contrib/bfd.uts b/test/contrib/bfd.uts index 7de9dd30681..39a467b115a 100644 --- a/test/contrib/bfd.uts +++ b/test/contrib/bfd.uts @@ -44,4 +44,34 @@ assert raw(p) == b'\x0e\xc8\x0e\xc8\x008\x00\x00 \xc4\x030\x11\x11\x11\x11"""";\ = BFD with OptionalAuth [Meticulous Keyed SHA1 Auth] [Build] p = UDP(sport=3784, dport=3784)/BFD(flags="A", optional_auth=OptionalAuth(auth_type=5)) -assert raw(p) == b'\x0e\xc8\x0e\xc8\x00<\x00\x00 \xc4\x034\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00\x05\x1c\x01\x00\x00\x00\x00\x00[\xaaa\xe4\xc9\xb9??\x06\x82%\x0bl\xf83\x1b~\xe6\x8f\xd8' \ No newline at end of file +assert raw(p) == b'\x0e\xc8\x0e\xc8\x00<\x00\x00 \xc4\x034\x11\x11\x11\x11"""";\x9a\xca\x00;\x9a\xca\x00;\x9a\xca\x00\x05\x1c\x01\x00\x00\x00\x00\x00[\xaaa\xe4\xc9\xb9??\x06\x82%\x0bl\xf83\x1b~\xe6\x8f\xd8' + += BFD without Auth flag - dissection should not inject phantom optional_auth (Issue #4937) + +a = UDP(sport=3784, dport=3784)/BFD() +p = UDP(raw(a)) +assert p[BFD].optional_auth is None +assert not p[BFD].flags.A + += BFD with non-Auth flags set - optional_auth should still be None + +a = UDP(sport=3784, dport=3784)/BFD(flags="DF") +p = UDP(raw(a)) +assert p[BFD].flags.D +assert p[BFD].flags.F +assert not p[BFD].flags.A +assert p[BFD].optional_auth is None + += BFD round-trip without auth preserves raw bytes + +a = UDP(sport=3784, dport=3784)/BFD() +raw1 = raw(a) +raw2 = raw(UDP(raw1)) +assert raw1 == raw2 + += BFD with Auth flag set - optional_auth should be present + +p = UDP(b'\x04\x00\x0e\xc8\x00\x29\x72\x31\x20\x44\x05\x21\x00\x00\x00\x01\x00\x00\x00\x00\x00\x0f\x42\x40\x00\x0f\x42\x40\x00\x00\x00\x00\x01\x09\x02\x73\x65\x63\x72\x65\x74\x4e\x0a\x90\x40') +assert p[BFD].flags.A +assert p[BFD].optional_auth is not None +assert isinstance(p[BFD].optional_auth, OptionalAuth) \ No newline at end of file diff --git a/test/contrib/cansocket_python_can.uts b/test/contrib/cansocket_python_can.uts index 78fa349d899..630c5d706d8 100644 --- a/test/contrib/cansocket_python_can.uts +++ b/test/contrib/cansocket_python_can.uts @@ -23,6 +23,9 @@ bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 typ = Load os ~ conf command +import sys +sys.path.append(".") +sys.path.append("test") import os import threading from subprocess import call @@ -618,3 +621,45 @@ if 0 != call(["sudo", "ip" ,"link", "delete", "vcan0"]): if 0 != call(["sudo", "ip" ,"link", "delete", "vcan1"]): raise Exception("vcan1 could not be deleted") + + ++ PythonCANSocket Extra Coverage (Platform Independent) + += _is_sw_filtered logic +from scapy.contrib.cansocket_python_can import _is_sw_filtered +assert _is_sw_filtered("slcan_0") +assert not _is_sw_filtered("socketcan_0") + += SocketsPool register / unregister / internal_send edge cases +import threading +from collections import deque +from scapy.contrib.cansocket_python_can import SocketsPool + +class DummyWrapper: + def __init__(self, name): + self.name = name + self.filters = None + self.lock = threading.Lock() + self.rx_queue = deque() + def _matches_filters(self, msg): return True + def shutdown(self): pass + +# internal_send not in pool +try: + SocketsPool.internal_send(DummyWrapper("none"), None) +except TypeError: + pass + +# register with interface (use virtual to avoid WinError on socketcan) +sock = DummyWrapper(None) +SocketsPool.register(sock, bustype="virtual", channel="vcan0") +assert sock.name == "virtual_vcan0" +SocketsPool.unregister(sock) + += PythonCANSocket.recv_raw with message +s = PythonCANSocket(bustype="virtual", channel="vcan0") +from can import Message as can_Message +msg = can_Message(arbitration_id=0x123, data=[1,2,3], is_extended_id=False) +s.can_iface.rx_queue.append(msg) +assert s.recv_raw()[1] is not None +s.close() diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts index f112563d62a..eef5b5e6d67 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -10,7 +10,7 @@ from io import BytesIO from scapy.layers.can import * from scapy.contrib.isotp import * from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler -from test.testsocket import TestSocket, SlowTestSocket, cleanup_testsockets +from test.testsocket import TestSocket, SlowTestSocket, USBTestSocket, cleanup_testsockets with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: exec(f.read()) @@ -1658,7 +1658,1274 @@ assert result is not None, "MF response not received (slcan, can_filters, thread assert result.data == expected -+ Cleanup ++ ISOTP socket reuse tests (TimeoutScheduler race fix verification) +# These tests verify the fix for a race condition in TimeoutScheduler +# that caused sr1() timeouts when reusing CAN adapters across multiple +# ISOTP socket open/close cycles (Python 3.12+ on Windows with slcan/candle). +# +# The root cause was a race window in _task(): between _peek_next() +# returning None (GRACE expired) and the `return` statement, schedule() +# could see _thread as not-None and skip starting a new thread. The old +# thread then died, orphaning the newly pushed handles. +# +# The fix: _task() now holds _mutex when deciding to die, atomically +# checking _handles one more time. If schedule() pushed a new handle, +# _task() sees it and stays alive instead of dying. + += TimeoutScheduler race fix: schedule() during GRACE-expiry is handled + +# Verifies the fix for the race condition where schedule() could run +# while _task() was about to die after GRACE expiration. +# +# The fix: _task() now holds _mutex when deciding to die and checks +# _handles one more time. If schedule() pushed a new handle while +# the thread was in the GRACE countdown, _task() sees it and stays +# alive instead of dying. +# +# This test exercises the real (fixed) code path: +# 1. Schedule a handle, let it fire, clear all handles +# 2. Wait for GRACE to expire (thread enters shutdown path) +# 3. Schedule a new handle — with the fix, the thread stays alive +# or schedule() starts a fresh thread +# 4. Assert the callback fires + +import time as _time +from threading import Thread as _Thread, Event as _Event + +# Clean slate +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + +callback_fired = _Event() + +# Schedule a dummy handle to start the thread, let it fire +TimeoutScheduler.schedule(0.001, lambda: None) +_time.sleep(0.02) +# Clear all handles — thread enters GRACE countdown +TimeoutScheduler.clear() +# Wait past GRACE so thread is in the process of dying or already dead +_time.sleep(TimeoutScheduler.GRACE + 0.05) + +# Now schedule a real callback — with the fix, this must succeed +TimeoutScheduler.schedule(0.01, callback_fired.set) + +# The callback MUST fire — this is the fix verification +assert callback_fired.wait(timeout=5.0), \ + "TimeoutScheduler race fix failed: callback did not fire " \ + "after scheduling during GRACE expiry" + +# Cleanup +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += TimeoutScheduler race fix: rapid schedule after close/reopen cycle + +# Verifies that rapidly closing and reopening ISOTP sockets doesn't +# orphan TimeoutScheduler handles. Schedules handles, cancels them +# (simulating socket close), then immediately schedules new ones +# (simulating socket reopen). All new callbacks must fire. + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + +for trial in range(5): + fired = _Event() + # Simulate socket open: schedule handles + h1 = TimeoutScheduler.schedule(0.005, lambda: None) + h2 = TimeoutScheduler.schedule(0.005, lambda: None) + _time.sleep(0.02) # let them fire + # Simulate socket close: cancel remaining + try: + h1.cancel() + except Exception: + pass + try: + h2.cancel() + except Exception: + pass + # Simulate socket reopen: schedule new handle immediately + TimeoutScheduler.schedule(0.01, fired.set) + assert fired.wait(timeout=5.0), \ + "Trial %d: callback did not fire after cancel/reschedule" % trial + +# Cleanup +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += TimeoutScheduler race fix: ISOTP close/reopen with GRACE expiry + +# End-to-end test: open an ISOTPSoftSocket, close it, wait for GRACE +# to expire, then open a new one. With the fix, the second socket's +# callbacks (can_recv, _send) must be executed by the scheduler. + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + # Iteration 1: works fine + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + stop = _Event() + def ecu_r1(_stim=stim, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + t1 = _Thread(target=ecu_r1) + t1.start() + resp1 = isock.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + stop.set() + t1.join(timeout=5) + assert resp1 is not None, "Iteration 1 should succeed" + # Wait past GRACE so thread fully enters shutdown path + _time.sleep(TimeoutScheduler.GRACE + 0.05) + # Iteration 2: open a new ISOTPSocket — scheduler must handle it + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock2: + stop2 = _Event() + def ecu_r2(_stim=stim, _stop=stop2): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + t2 = _Thread(target=ecu_r2) + t2.start() + resp2 = isock2.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + stop2.set() + t2.join(timeout=5) + assert resp2 is not None, \ + "Iteration 2 timed out — TimeoutScheduler race fix failed" + +# Cleanup +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += TimeoutScheduler race fix: stress test across GRACE boundaries + +# Stress test: rapidly schedule/wait/clear across many iterations to +# exercise the GRACE-expiry atomic check under real thread scheduling. +# Each iteration lets the thread die (or nearly die), then schedules +# a new callback. All callbacks must fire. + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + +_missed = [] +for trial in range(20): + fired = _Event() + # Schedule and immediately let it fire + TimeoutScheduler.schedule(0.001, lambda: None) + _time.sleep(0.005) + # Clear handles — thread begins GRACE countdown + TimeoutScheduler.clear() + # Wait approximately GRACE time (sometimes under, sometimes over) + # to exercise both the "thread still alive" and "thread dead" paths + if trial % 2 == 0: + _time.sleep(TimeoutScheduler.GRACE + 0.02) + else: + _time.sleep(TimeoutScheduler.GRACE * 0.8) + # Schedule a new callback — must fire regardless of thread state + TimeoutScheduler.schedule(0.01, fired.set) + if not fired.wait(timeout=5.0): + _missed.append(trial) + +assert not _missed, \ + "Callbacks failed to fire on trials: %s" % _missed + +# Cleanup +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: both CANSocket and ISOTPSocket recreated each iteration + +# Reproduces the user's exact bug pattern: +# for m in messages: +# with CANSocket() as csock: +# with ISOTPSocket(csock) as isock: +# resp = isock.sr1(...) +# +# On Python 3.12+ on Windows, the 2nd/3rd iteration times out because +# the TimeoutScheduler thread dies during the close/reopen gap. +# On Linux, the race window is small so this test exercises the code +# path without deterministically triggering the race. + +def run_isotp_reuse_both_sockets_recreated(num_iterations=3): + """Run sr1 in a loop, recreating both sockets each time.""" + results = [] + for i in range(num_iterations): + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + stop = _Event() + def ecu_responder(_stim=stim, _stop=stop): + """Echo back a SF response for any received request.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + ecu_thread = _Thread(target=ecu_responder) + ecu_thread.start() + try: + resp = isock.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + results.append(resp) + finally: + stop.set() + ecu_thread.join(timeout=5) + # Brief pause between iterations — on 3.11.9 this works fine, + # on 3.12+ the TimeoutScheduler thread may die here + _time.sleep(0.05) + return results + +results = run_isotp_reuse_both_sockets_recreated(3) +for i, r in enumerate(results): + assert r is not None, \ + "Iteration %d timed out (both sockets recreated)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: CANSocket kept open, only ISOTPSocket recreated + +# Same bug but with the CANSocket kept across iterations. +# This proves the bug is in the ISOTP/TimeoutScheduler layer, +# not in the SocketsPool/CANSocket layer. + +def run_isotp_reuse_can_socket_kept(num_iterations=3): + """Run sr1 in a loop, keeping the CANSocket and recreating ISOTPSocket.""" + results = [] + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + for i in range(num_iterations): + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + stop = _Event() + def ecu_responder(_stim=stim, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + ecu_thread = _Thread(target=ecu_responder) + ecu_thread.start() + try: + resp = isock.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + results.append(resp) + finally: + stop.set() + ecu_thread.join(timeout=5) + # Brief pause — TimeoutScheduler thread may die here + _time.sleep(0.05) + return results + +results = run_isotp_reuse_can_socket_kept(3) +for i, r in enumerate(results): + assert r is not None, \ + "Iteration %d timed out (CANSocket kept, ISOTPSocket recreated)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: rapid open/close/reopen without GRACE delay + +# Variant where we do NOT wait between iterations. This tests the case +# where the TimeoutScheduler thread is still in the GRACE wait when new +# handles are scheduled. + +def run_isotp_reuse_rapid(num_iterations=3): + """Run sr1 in a loop with no delay between close and reopen.""" + results = [] + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + for i in range(num_iterations): + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + stop = _Event() + def ecu_responder(_stim=stim, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + ecu_thread = _Thread(target=ecu_responder) + ecu_thread.start() + try: + resp = isock.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + results.append(resp) + finally: + stop.set() + ecu_thread.join(timeout=5) + # NO delay — immediate reopen + return results + +results = run_isotp_reuse_rapid(3) +for i, r in enumerate(results): + assert r is not None, \ + "Iteration %d timed out (rapid reopen, no delay)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: CANSocket recreated with delay past GRACE period + +# Variant where we wait longer than GRACE (100ms) between iterations, +# ensuring the TimeoutScheduler thread has fully died. schedule() must +# reliably start a new thread. + +def run_isotp_reuse_post_grace(num_iterations=3): + """Run sr1 in a loop, waiting past GRACE between iterations.""" + results = [] + for i in range(num_iterations): + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + stop = _Event() + def ecu_responder(_stim=stim, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(CAN(identifier=0x7eb, + data=dhex("02 50 03"))) + ecu_thread = _Thread(target=ecu_responder) + ecu_thread.start() + try: + resp = isock.sr1(ISOTP(b'\x10\x03'), timeout=5, verbose=0) + results.append(resp) + finally: + stop.set() + ecu_thread.join(timeout=5) + # Wait past GRACE so thread is fully dead + _time.sleep(TimeoutScheduler.GRACE + 0.05) + return results + +results = run_isotp_reuse_post_grace(3) +for i, r in enumerate(results): + assert r is not None, \ + "Iteration %d timed out (post-GRACE delay)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: MF (multi-frame) response across iterations + +# The original bug report uses UDS 0x1003 (DiagnosticSessionControl) +# which returns a multi-frame response. This test verifies that MF +# responses work across multiple open/close cycles, using the same +# ECU simulation pattern as the cartesian product tests above. + +def run_isotp_mf_reuse(num_iterations=3, keep_can_socket=True): + """Run MF sr1 exchange in a loop.""" + response_data = dhex("620001666c61677b5544535f444154415f524541447d") + results = [] + cans_ctx = None + stim_ctx = None + if keep_can_socket: + cans_ctx = TestSocket(CAN) + stim_ctx = TestSocket(CAN) + cans_ctx.pair(stim_ctx) + for i in range(num_iterations): + if not keep_can_socket: + cans_ctx = TestSocket(CAN) + stim_ctx = TestSocket(CAN) + cans_ctx.pair(stim_ctx) + cans = cans_ctx + stim = stim_ctx + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + fc_received = _Event() + stop = _Event() + def ecu_mf_responder(_stim=stim, _fc=fc_received, _stop=stop): + """Send a multi-frame response after receiving FC.""" + _time.sleep(0.05) + _stim.send(CAN(identifier=0x7eb, + data=dhex("1016620001666c61"))) + _fc.wait(timeout=10.0) + if not _fc.is_set(): + return + _time.sleep(0.008) + _stim.send(CAN(identifier=0x7eb, + data=dhex("21677b5544535f44"))) + _time.sleep(0.010) + _stim.send(CAN(identifier=0x7eb, + data=dhex("224154415f524541"))) + _time.sleep(0.010) + _stim.send(CAN(identifier=0x7eb, + data=dhex("23447d"))) + with TestSocket(CAN) as ecu_mon: + cans.pair(ecu_mon) + def fc_watcher(_mon=ecu_mon, _fc=fc_received, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_mon], 0.1): + pkt = _mon.recv() + if pkt is not None and \ + pkt.identifier == 0x7e3 and \ + len(pkt.data) >= 1 and \ + bytes(pkt.data)[0] == 0x30: + _fc.set() + return + ecu_thread = _Thread(target=ecu_mf_responder) + fc_thread = _Thread(target=fc_watcher) + ecu_thread.start() + fc_thread.start() + try: + resp = isock.sr1(ISOTP(data=dhex("220001")), + retry=0, timeout=10.0, verbose=0) + results.append(resp) + finally: + stop.set() + fc_received.set() + ecu_thread.join(timeout=5) + fc_thread.join(timeout=5) + # Unpair ecu_mon before its context manager closes it; + # otherwise stim would try to send to a closed pipe + # on the next iteration since pair() is bidirectional. + try: + cans.paired_sockets.remove(ecu_mon) + except ValueError: + pass + # Cleanup scheduler between iterations to match real-world + # pattern where user code doesn't explicitly manage the scheduler + _time.sleep(0.05) + if not keep_can_socket: + cans_ctx.close() + stim_ctx.close() + if keep_can_socket: + cans_ctx.close() + stim_ctx.close() + return results, response_data + +results, expected = run_isotp_mf_reuse(3, keep_can_socket=True) +for i, r in enumerate(results): + assert r is not None, \ + "MF iteration %d timed out (CANSocket kept)" % (i + 1) + assert r.data == expected, \ + "MF iteration %d data mismatch (CANSocket kept)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += ISOTP socket reuse: MF response, both sockets recreated each iteration + +results, expected = run_isotp_mf_reuse(3, keep_can_socket=False) +for i, r in enumerate(results): + assert r is not None, \ + "MF iteration %d timed out (both recreated)" % (i + 1) + assert r.data == expected, \ + "MF iteration %d data mismatch (both recreated)" % (i + 1) + +# Cleanup scheduler +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + + += Orphan can_recv guard: closed ISOTP socket must not consume bus frames + +# Verifies the fix for the orphan-callback bug: +# When close() races with can_recv() on the TimeoutScheduler thread, +# the old handle can fire one last time after self.closed is set. +# Without the guard at the top of can_recv(), the orphan callback +# would call select() → multiplex_rx_packets() → recv() and +# consume response frames from the shared CAN bus that belong to +# the NEXT ISOTPSocket session. This causes the next sr1() to +# time out. +# +# This test deterministically reproduces the race: +# 1. Create an ISOTP socket, get its ISOTPSocketImplementation +# 2. Close the ISOTP socket (sets impl.closed = True) +# 3. Inject a response frame on the shared CAN bus +# 4. Call impl.can_recv() as if it were an orphan callback +# 5. Assert the CAN bus frame was NOT consumed + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + isock = ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) + impl = isock.impl + # Stop the background callbacks so we control timing + _ts = TimeoutScheduler._thread + TimeoutScheduler.clear() + if _ts is not None: + _ts.join(timeout=5) + # Close the ISOTP socket (simulates normal close) + isock.close() + assert impl.closed is True + # Now inject a response frame on the CAN bus + stim.send(CAN(identifier=0x7eb, data=dhex("02 50 03"))) + # Simulate the orphan callback firing after close + impl.can_recv() + # The CAN frame must still be on the bus (not consumed by orphan) + assert TestSocket.select([cans], 0.1), \ + "Frame was consumed by orphan can_recv — not available on bus" + pkt = cans.recv() + assert pkt is not None, "Frame was consumed by orphan can_recv" + assert pkt.identifier == 0x7eb + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Adapter buffer overflow: shared CANSocket, USB adapter FIFO fills between sessions + +# Reproduces the USB adapter hardware buffer overflow bug: +# +# Pattern: with CANSocket(): for msg: with ISOTPSocket(): +# +# Between close() of one ISOTPSocket and __init__() of the next, +# nobody calls select() on the CANSocket. On USB adapters (candle, +# cantact) the hardware endpoint FIFO is small (32-128 frames). +# Background CAN traffic fills it while no ISOTP socket is active. +# When the next ISOTPSocket sends a request, the ECU's response +# frame arrives but the FIFO is already full → silently dropped. +# slcan doesn't have this issue because the OS serial buffer is +# much larger (4096+ bytes). +# +# The fix: __init__() drains the adapter buffer via select() before +# scheduling callbacks, and close() drains again after setting +# self.closed. Both drains call multiplex_rx_packets() which +# moves frames from hardware FIFO to the software rx_queue, +# freeing space for the ECU's response. +# +# This test uses USBTestSocket with a small hw_fifo_size to make +# the overflow deterministic even without real hardware. + +def run_usb_buffer_overflow_test(num_iterations=5, hw_fifo_size=8, + bg_frames_per_gap=12, + disable_drain=False): + """Run sr1 in a loop with a shared USB-like CANSocket. + Between iterations, inject bg_frames_per_gap background frames + to fill/overflow the USB adapter's hardware FIFO. + If disable_drain is True, re-fill the FIFO after __init__ drains + it, reproducing the pre-fix behavior where no drain occurred. + """ + import time as _time + from threading import Thread as _Thread, Event as _Event + from scapy.layers.can import CAN as _CAN + from scapy.contrib.isotp import ISOTP as _ISOTP + from scapy.contrib.isotp.isotp_soft_socket import ISOTPSoftSocket as _ISOTPSoftSocket + from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler as _TS + from test.testsocket import TestSocket as _TestSocket, USBTestSocket as _USBTestSocket + _dhex = bytes.fromhex + results = [] + bg_ids = [0x062, 0x024, 0x039, 0x077, 0x098, 0x150] + with _USBTestSocket(_CAN, hw_fifo_size=hw_fifo_size) as cans, \ + _TestSocket(_CAN) as stim: + cans.pair(stim) + for iteration in range(num_iterations): + # Inject background traffic between sessions. + # On real hardware this comes from other ECUs on the bus. + # The frames go into the USB adapter's hardware FIFO. + # If nobody drains it, the FIFO overflows. + if iteration > 0: + for j in range(bg_frames_per_gap): + bid = bg_ids[j % len(bg_ids)] + stim.send(_CAN(identifier=bid, data=bytes(8))) + with _ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + if disable_drain and iteration > 0: + # Undo the drain that __init__ performed by + # re-injecting the same amount of background + # frames back into the FIFO. This simulates + # the old code that didn't drain at all. + for j in range(bg_frames_per_gap): + bid = bg_ids[j % len(bg_ids)] + stim.send(_CAN(identifier=bid, data=bytes(8))) + stop = _Event() + def ecu_responder(_stim=stim, _stop=stop, _it=iteration): + while not _stop.is_set(): + if _TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + _stim.send(_CAN(identifier=0x7eb, + data=_dhex("02 50 03"))) + ecu_thread = _Thread(target=ecu_responder) + ecu_thread.start() + try: + resp = isock.sr1(_ISOTP(b'\x10\x03'), timeout=2, verbose=0) + results.append(resp) + finally: + stop.set() + ecu_thread.join(timeout=5) + _time.sleep(0.02) + dropped = cans.dropped_count + return results, dropped + +# Test 1: With the fix (current code), all iterations should succeed. +# The drains in __init__/close() keep the FIFO from overflowing. +results, dropped = run_usb_buffer_overflow_test( + num_iterations=5, hw_fifo_size=8, bg_frames_per_gap=12, + disable_drain=False) +for i, r in enumerate(results): + assert r is not None, \ + "Iteration %d timed out (USB buffer overflow, drain enabled)" % (i + 1) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Adapter buffer overflow: without drain, USB FIFO overflows and responses are lost + +# Test 2: Simulate the pre-fix behavior by re-filling the FIFO after +# __init__ drains it. This proves that without the drain, the USB +# adapter FIFO overflows and ECU responses are dropped. +results, dropped = run_usb_buffer_overflow_test( + num_iterations=5, hw_fifo_size=8, bg_frames_per_gap=12, + disable_drain=True) +# With the drain disabled (simulated), at least one iteration should +# fail due to FIFO overflow. On real hardware this is the 50-50 +# failure pattern seen with candle/cantact adapters. +failures = sum(1 for r in results if r is None) +assert failures > 0 or dropped > 0, \ + "Expected at least one timeout or FIFO drop when drain is disabled, " \ + "got %d failures, %d drops" % (failures, dropped) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + + += Stress test: hundreds of SF exchanges on persistent ISOTPSoftSocket + +# Exercises a single ISOTPSoftSocket for many request-response cycles +# without closing/reopening. The socket must reliably dispatch all +# exchanges via the same TimeoutScheduler callbacks. +# Uses short timeout (2s) per exchange to surface any timing bugs. + +import time as _time +from threading import Thread as _Thread, Event as _Event + +NUM_SF_EXCHANGES = 200 + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + exchange_count = [0] + def sf_ecu(_stim=stim, _stop=stop, _count=exchange_count): + """Auto-respond to any SF request with a SF response.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + raw = bytes(pkt.data) + # Echo back the service ID + 0x40 + if len(raw) >= 2: + sid = raw[1] + _stim.send(CAN(identifier=0x7eb, + data=bytes([0x02, sid + 0x40, 0x03]))) + _count[0] += 1 + ecu_thread = _Thread(target=sf_ecu) + ecu_thread.start() + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + failures = [] + for i in range(NUM_SF_EXCHANGES): + resp = isock.sr1(ISOTP(b'\x10\x03'), timeout=2, verbose=0) + if resp is None: + failures.append(i) + stop.set() + ecu_thread.join(timeout=10) + +assert len(failures) == 0, \ + "SF stress test: %d/%d timed out, first failures: %s" % ( + len(failures), NUM_SF_EXCHANGES, failures[:10]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Stress test: hundreds of MF exchanges on persistent ISOTPSoftSocket + +# Same pattern but with multi-frame responses (4 CF). This exercises +# the full ISOTP state machine (FF → FC → CF×3) across many cycles +# on the same socket instance. + +NUM_MF_EXCHANGES = 100 + +def run_mf_stress(num_exchanges): + """Run num_exchanges MF request/response cycles on one ISOTPSoftSocket.""" + expected_data = dhex("620001666c61677b5544535f444154415f524541447d") + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + with TestSocket(CAN) as ecu_mon: + cans.pair(ecu_mon) + def mf_ecu(_stim=stim, _mon=ecu_mon, _stop=stop): + """For each request, send an MF response (FF + 3 CF).""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e3: + # Send FF + _stim.send(CAN(identifier=0x7eb, + data=dhex("1016620001666c61"))) + # Wait for FC from tester + fc_seen = False + deadline = _time.monotonic() + 5.0 + while not _stop.is_set() and _time.monotonic() < deadline: + if TestSocket.select([_mon], 0.05): + fp = _mon.recv() + if fp is not None and \ + fp.identifier == 0x7e3 and \ + len(fp.data) >= 1 and \ + bytes(fp.data)[0] == 0x30: + fc_seen = True + break + if not fc_seen: + continue + _time.sleep(0.002) + _stim.send(CAN(identifier=0x7eb, + data=dhex("21677b5544535f44"))) + _time.sleep(0.002) + _stim.send(CAN(identifier=0x7eb, + data=dhex("224154415f524541"))) + _time.sleep(0.002) + _stim.send(CAN(identifier=0x7eb, + data=dhex("23447d"))) + ecu_thread = _Thread(target=mf_ecu) + ecu_thread.start() + with ISOTPSoftSocket(cans, tx_id=0x7e3, rx_id=0x7eb) as isock: + failures = [] + mismatches = [] + for i in range(num_exchanges): + resp = isock.sr1(ISOTP(data=dhex("220001")), + retry=0, timeout=5, verbose=0) + if resp is None: + failures.append(i) + elif resp.data != expected_data: + mismatches.append(i) + stop.set() + ecu_thread.join(timeout=10) + # Unpair ecu_mon to avoid stale references + try: + cans.paired_sockets.remove(ecu_mon) + except ValueError: + pass + return failures, mismatches + +failures, mismatches = run_mf_stress(NUM_MF_EXCHANGES) +assert len(failures) == 0, \ + "MF stress test: %d/%d timed out, first failures: %s" % ( + len(failures), NUM_MF_EXCHANGES, failures[:10]) +assert len(mismatches) == 0, \ + "MF stress test: %d/%d data mismatch, first: %s" % ( + len(mismatches), NUM_MF_EXCHANGES, mismatches[:10]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Stress test: UDS-scanner-style varying service IDs on persistent socket (SF) + +# Mimics UDS_ServiceEnumerator: iterates through different service IDs +# on the SAME ISOTPSoftSocket without closing/reopening. Each sr1() +# sends a different UDS service request and the ECU simulator replies +# with the matching positive response. This exercises the rx_queue +# across varying hashret/answers pairs — a late response from service +# N could confuse service N+1 if state isn't handled properly. + +import time as _time +from threading import Thread as _Thread, Event as _Event + +SCAN_RANGE = list(range(0x10, 0x3F)) # 47 services +NUM_SCAN_CYCLES = 5 # Repeat the scan range 5 times = 235 total exchanges + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + def uds_ecu_sf(_stim=stim, _stop=stop): + """ECU that responds to each UDS service with positive response.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e0: + raw = bytes(pkt.data) + if len(raw) >= 2: + pci_len = raw[0] + sid = raw[1] + # Positive response: SID + 0x40 + resp_sid = (sid + 0x40) & 0xFF + _stim.send(CAN(identifier=0x7e8, + data=bytes([0x02, resp_sid, 0x00]))) + ecu_thread = _Thread(target=uds_ecu_sf) + ecu_thread.start() + with ISOTPSoftSocket(cans, tx_id=0x7e0, rx_id=0x7e8) as isock: + failures = [] + wrong_resp = [] + total = 0 + for cycle in range(NUM_SCAN_CYCLES): + for sid in SCAN_RANGE: + req = ISOTP(data=bytes([sid, 0x00])) + resp = isock.sr1(req, timeout=2, verbose=0) + if resp is None: + failures.append((cycle, sid, total)) + elif len(resp.data) < 1 or resp.data[0] != ((sid + 0x40) & 0xFF): + wrong_resp.append((cycle, sid, total, bytes(resp.data))) + total += 1 + stop.set() + ecu_thread.join(timeout=10) + +assert len(failures) == 0, \ + "UDS SF scan stress: %d/%d timed out, first: %s" % ( + len(failures), total, failures[:5]) +assert len(wrong_resp) == 0, \ + "UDS SF scan stress: %d/%d wrong response, first: %s" % ( + len(wrong_resp), total, wrong_resp[:5]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Stress test: UDS-scanner-style varying service IDs on persistent socket (MF) + +# Same pattern but some services return multi-frame responses. +# Services 0x22 (ReadDataByIdentifier) and 0x19 (ReadDTCInformation) +# return long MF responses; all others return SF. This tests the +# ISOTP state machine transitioning between SF and MF responses across +# many consecutive sr1() calls on the same socket. + +MF_SIDS = {0x22, 0x19} # Services that return multi-frame responses +NUM_MF_SCAN_CYCLES = 3 +MF_SCAN_RANGE = [0x10, 0x11, 0x19, 0x22, 0x27, 0x2E, 0x31, 0x3E] + +def run_uds_mf_scan_stress(num_cycles, scan_range, mf_sids): + """Scan with mixed SF/MF responses on a persistent socket.""" + mf_payload = dhex("0001AABBCCDD112233445566778899") # 15 bytes + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + with TestSocket(CAN) as ecu_mon: + cans.pair(ecu_mon) + def uds_ecu_mf(_stim=stim, _mon=ecu_mon, _stop=stop): + """ECU with mixed SF/MF responses per service ID.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is None or not hasattr(pkt, 'identifier'): + continue + if pkt.identifier != 0x7e0: + continue + raw = bytes(pkt.data) + if len(raw) < 2: + continue + # Only process Single Frame requests (PCI type 0). + # Ignore FC frames (PCI type 3 = 0x3X) that the + # tester sends back during MF exchanges. + pci_type = (raw[0] >> 4) & 0x0F + if pci_type != 0: + continue + sid = raw[1] + resp_sid = (sid + 0x40) & 0xFF + if sid in mf_sids: + # Multi-frame response: FF + CFs + # Build 16-byte payload: resp_sid + 15 bytes + resp_data = bytes([resp_sid]) + mf_payload + ff_data = bytes([0x10, len(resp_data)]) + resp_data[:6] + _stim.send(CAN(identifier=0x7e8, data=ff_data)) + # Wait for FC + fc_seen = False + deadline = _time.monotonic() + 5.0 + while not _stop.is_set() and _time.monotonic() < deadline: + if TestSocket.select([_mon], 0.05): + fp = _mon.recv() + if fp is not None and \ + fp.identifier == 0x7e0 and \ + len(fp.data) >= 1 and \ + (bytes(fp.data)[0] >> 4) == 3: + fc_seen = True + break + if not fc_seen: + continue + _time.sleep(0.002) + cf1 = bytes([0x21]) + resp_data[6:13] + _stim.send(CAN(identifier=0x7e8, data=cf1)) + _time.sleep(0.002) + cf2 = bytes([0x22]) + resp_data[13:16] + _stim.send(CAN(identifier=0x7e8, data=cf2)) + else: + # Single-frame response + _stim.send(CAN(identifier=0x7e8, + data=bytes([0x02, resp_sid, 0x00]))) + ecu_thread = _Thread(target=uds_ecu_mf) + ecu_thread.start() + with ISOTPSoftSocket(cans, tx_id=0x7e0, rx_id=0x7e8) as isock: + failures = [] + wrong_resp = [] + total = 0 + for cycle in range(num_cycles): + for sid in scan_range: + req = ISOTP(data=bytes([sid, 0x00])) + resp = isock.sr1(req, timeout=5, verbose=0) + expected_sid = (sid + 0x40) & 0xFF + if resp is None: + failures.append((cycle, sid, total)) + elif len(resp.data) < 1 or resp.data[0] != expected_sid: + wrong_resp.append((cycle, sid, total, bytes(resp.data))) + total += 1 + stop.set() + ecu_thread.join(timeout=10) + try: + cans.paired_sockets.remove(ecu_mon) + except ValueError: + pass + return failures, wrong_resp, total + +failures, wrong_resp, total = run_uds_mf_scan_stress( + NUM_MF_SCAN_CYCLES, MF_SCAN_RANGE, MF_SIDS) +assert len(failures) == 0, \ + "UDS MF scan stress: %d/%d timed out, first: %s" % ( + len(failures), total, failures[:5]) +assert len(wrong_resp) == 0, \ + "UDS MF scan stress: %d/%d wrong response, first: %s" % ( + len(wrong_resp), total, wrong_resp[:5]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Stress test: close/reopen cycles between service groups (scanner reconnect pattern) + +# Mimics UDS_Scanner reconnect_handler: after scanning one ECU state, +# close the ISOTPSoftSocket and create a new one (same CANSocket). +# This is the pattern that triggers the TimeoutScheduler race and +# orphan-callback bugs at scale. Each group does 10 service probes, +# then close/reopen. + +NUM_GROUPS = 20 +PROBES_PER_GROUP = 10 +GROUP_SCAN_RANGE = list(range(0x10, 0x10 + PROBES_PER_GROUP)) + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + def group_ecu(_stim=stim, _stop=stop): + """ECU that responds to any service probe.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e0: + raw = bytes(pkt.data) + if len(raw) >= 2: + sid = raw[1] + resp_sid = (sid + 0x40) & 0xFF + _stim.send(CAN(identifier=0x7e8, + data=bytes([0x02, resp_sid, 0x00]))) + ecu_thread = _Thread(target=group_ecu) + ecu_thread.start() + failures = [] + total = 0 + for group in range(NUM_GROUPS): + with ISOTPSoftSocket(cans, tx_id=0x7e0, rx_id=0x7e8) as isock: + for sid in GROUP_SCAN_RANGE: + req = ISOTP(data=bytes([sid, 0x00])) + resp = isock.sr1(req, timeout=2, verbose=0) + if resp is None: + failures.append((group, sid, total)) + total += 1 + # Brief pause between groups — mimics scanner state transition + _time.sleep(0.02) + stop.set() + ecu_thread.join(timeout=10) + +assert len(failures) == 0, \ + "Reconnect stress: %d/%d timed out, first: %s" % ( + len(failures), total, failures[:10]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + += Stress test: rapid close/reopen with no delay (worst-case reconnect) + +# Same as above but with ZERO delay between groups. This maximises +# the race window where the TimeoutScheduler thread may die during +# close and the next ISOTPSocket.__init__ needs it alive. + +NUM_RAPID_GROUPS = 30 +RAPID_PROBES = 5 + +with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + def rapid_ecu(_stim=stim, _stop=stop): + """ECU for rapid reconnect test.""" + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e0: + raw = bytes(pkt.data) + if len(raw) >= 2: + sid = raw[1] + _stim.send(CAN(identifier=0x7e8, + data=bytes([0x02, (sid + 0x40) & 0xFF, 0x00]))) + ecu_thread = _Thread(target=rapid_ecu) + ecu_thread.start() + failures = [] + total = 0 + for group in range(NUM_RAPID_GROUPS): + with ISOTPSoftSocket(cans, tx_id=0x7e0, rx_id=0x7e8) as isock: + for probe in range(RAPID_PROBES): + sid = 0x10 + (probe % 0x30) + req = ISOTP(data=bytes([sid, 0x00])) + resp = isock.sr1(req, timeout=2, verbose=0) + if resp is None: + failures.append((group, sid, total)) + total += 1 + # NO delay — immediate close/reopen + stop.set() + ecu_thread.join(timeout=10) + +assert len(failures) == 0, \ + "Rapid reconnect stress: %d/%d timed out, first: %s" % ( + len(failures), total, failures[:10]) + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +if _ts is not None: + _ts.join(timeout=5) + + += TimeoutScheduler thread death recovery: stale _thread reference must not block new threads + +# Simulates the scenario where the TimeoutScheduler thread dies from a +# BaseException (or any unexpected exit) that leaves _thread as a stale +# reference. The schedule() fix detects the dead thread via is_alive() +# and starts a fresh one, so ISOTP callbacks resume normally. + +import time as _time +from threading import Thread as _Thread, Event as _Event + +# Clean slate +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +_ts and _ts.join(timeout=5) + +# Schedule a simple callback to get the thread running +_fired = _Event() +def _dummy_cb(_e=_fired): + _e.set() + +TimeoutScheduler.schedule(0, _dummy_cb) +_fired.wait(timeout=2) +assert _fired.is_set(), "Initial callback did not fire" + +# Wait for the thread to go idle and die (GRACE = 0.1s) +_time.sleep(0.3) + +# Now simulate a stale _thread: set _thread to a dead Thread object. +# This mimics what would happen if _task() exited without clearing +# _thread (the bug that the finally block fixes). +with TimeoutScheduler._mutex: + _dead = _Thread(target=lambda: None) + _dead.start() + _dead.join() + assert not _dead.is_alive() + TimeoutScheduler._thread = _dead + +# schedule() should detect the dead thread and start a new one +_fired2 = _Event() +def _recovery_cb(_e=_fired2): + _e.set() + +TimeoutScheduler.schedule(0, _recovery_cb) +_fired2.wait(timeout=2) +assert _fired2.is_set(), \ + "Recovery callback did not fire — schedule() failed to detect dead thread" + +# Verify ISOTP sockets still work after thread recovery +def run_isotp_after_recovery(): + with TestSocket(CAN) as cans, TestSocket(CAN) as stim: + cans.pair(stim) + stop = _Event() + def recovery_ecu(_stim=stim, _stop=stop): + while not _stop.is_set(): + if TestSocket.select([_stim], 0.05): + try: + pkt = _stim.recv() + except Exception: + break + if pkt is not None and hasattr(pkt, 'identifier'): + if pkt.identifier == 0x7e0: + raw = bytes(pkt.data) + if len(raw) >= 2: + sid = raw[1] + _stim.send(CAN(identifier=0x7e8, + data=bytes([0x02, (sid + 0x40) & 0xFF, 0x00]))) + ecu_thread = _Thread(target=recovery_ecu) + ecu_thread.start() + with ISOTPSoftSocket(cans, tx_id=0x7e0, rx_id=0x7e8) as isock: + for sid in [0x10, 0x11, 0x22]: + req = ISOTP(data=bytes([sid, 0x00])) + resp = isock.sr1(req, timeout=2, verbose=0) + assert resp is not None, \ + "ISOTP sr1 failed for SID 0x%02x after thread recovery" % sid + stop.set() + ecu_thread.join(timeout=10) + +run_isotp_after_recovery() + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +_ts and _ts.join(timeout=5) + + ++ ISOTPSoftSocket Extra Coverage + += ISOTPSocketImplementation.can_send FD padding +with ISOTPSoftSocket(TestSocket(CAN), fd=True, padding=True) as s: + s.impl.can_send(b"\x00"*10) # Should pad to 12 + += ISOTPSocketImplementation.on_can_recv identifier mismatch +with ISOTPSoftSocket(TestSocket(CAN), rx_id=0x123) as s: + s.impl.on_can_recv(CAN(identifier=0x124, data=b"\x00")) + assert s.impl.filter_warning_emitted + += _rx_timer_handler timeout reset +with ISOTPSoftSocket(TestSocket(CAN), rx_id=0x123) as s: + s.impl.rx_state = 2 # ISOTP_WAIT_DATA + s.impl.rx_start_time = TimeoutScheduler._time() + s.impl._rx_timer_handler() + assert s.impl.rx_state == 2 # Still waiting due to extension + += _tx_timer_handler edge cases +with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x123) as s: + s.impl.tx_state = 1 # ISOTP_SENDING + s.impl.tx_buf = None + s.impl._tx_timer_handler() + assert s.impl.tx_state == 0 # ISOTP_IDLE + += _recv_fc / _recv_sf / _recv_ff / _recv_cf edge cases +with ISOTPSoftSocket(TestSocket(CAN), rx_id=0x123) as s: + # _recv_fc short + s.impl.tx_state = 2 # ISOTP_WAIT_FC + s.impl._recv_fc(b"\x30\x00") + assert s.impl.tx_state == 0 + # _recv_fc unknown + s.impl.tx_state = 2 + s.impl._recv_fc(b"\x3F\x00\x00") + assert s.impl.tx_state == 0 + # _recv_sf FD + s.impl.fd = True + s.impl._recv_sf(b"\x00\x01\xAA", 1.23) + # _recv_ff short + s.impl._recv_ff(b"\x10\x00\x00\x00\x00\x00", 1.23) + assert s.impl.rx_state == 0 + # _recv_ff 32-bit length + s.impl._recv_ff(b"\x10\x00\x00\x00\x00\x0A\xAA\xBB\xCC\xDD", 1.23) + assert s.impl.rx_len == 10 + # _recv_cf various + s.impl.rx_state = 3 # ISOTP_WAIT_DATA + s.impl.rx_ll_dl = 8 + s.impl._recv_cf(b"\x21\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11") # Too long + assert s.impl.rx_state == 3 + s.impl.rx_sn = 2 + s.impl._recv_cf(b"\x21\xAA") # Wrong SN + assert s.impl.rx_state == 0 + += begin_send busy / too much data +with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x123) as s: + s.impl.tx_state = 1 + s.impl.begin_send(b"data") # Busy + s.impl.tx_state = 0 + s.impl.begin_send(b"A" * 5000) # Too much data + + += Delete testsockets + +_ts = TimeoutScheduler._thread +TimeoutScheduler.clear() +_ts and _ts.join(timeout=5) = Delete testsockets diff --git a/test/contrib/j1939.uts b/test/contrib/j1939.uts new file mode 100644 index 00000000000..6e0166ba02f --- /dev/null +++ b/test/contrib/j1939.uts @@ -0,0 +1,2074 @@ +% Regression tests for SAE J1939 protocol +~ not_pypy linux + +# More information at http://www.secdev.org/projects/UTscapy/ + +############ +############ ++ Setup +~ conf + += Load J1939 module + +load_contrib("j1939", globals_dict=globals()) +import socket +from scapy.contrib.j1939 import ( + J1939, J1939_CAN, + J1939_TP_CM, J1939_TP_CM_RTS, J1939_TP_CM_CTS, J1939_TP_CM_ACK, + J1939_TP_CM_BAM, J1939_TP_CM_ABORT, J1939_TP_DT, + J1939_BROADCAST_ADDR, + J1939_PGN_TP_CM, J1939_PGN_TP_DT, + J1939_TP_CTRL_RTS, J1939_TP_CTRL_CTS, J1939_TP_CTRL_ACK, + J1939_TP_CTRL_BAM, J1939_TP_CTRL_ABORT, + can_id_to_j1939, j1939_to_can_id, pgn_from_fields, dst_from_fields, + pgn_is_pdu1, +) + +J1939_NO_ADDR = socket.J1939_NO_ADDR +J1939_NO_PGN = socket.J1939_NO_PGN +J1939_NO_NAME = socket.J1939_NO_NAME +J1939_IDLE_ADDR = socket.J1939_IDLE_ADDR + += hexadecimal helper + +dhex = bytes.fromhex + ++ J1939_CAN inherits from CAN + += J1939_CAN is a subclass of CAN +from scapy.layers.can import CAN +assert issubclass(J1939_CAN, CAN) + += J1939_CAN dispatch_hook always returns J1939_CAN +raw_j1939_frame = bytes.fromhex('98feca0008000000ffffffffffffffff') +assert J1939_CAN.dispatch_hook(raw_j1939_frame) is J1939_CAN + += J1939_CAN instantiation does not redirect to CAN +pkt = J1939_CAN(raw_j1939_frame) +assert isinstance(pkt, J1939_CAN), "Expected J1939_CAN, got %s" % type(pkt).__name__ +assert pkt.pgn == 0xFECA +assert pkt.src == 0x00 + += J1939_CAN inherits pre_dissect / post_build (swap-bytes) +import struct as _struct +from scapy.config import conf as _conf + +# Round-trip: build with swap-bytes=True (post_build swaps), parse with swap-bytes=True (pre_dissect unswaps) +_conf.contribs['CAN']['swap-bytes'] = False +raw_unswapped = bytes(J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, src=0x00)) + +_conf.contribs['CAN']['swap-bytes'] = True +p_sw = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +raw_sw = bytes(p_sw) +# post_build (swap-bytes=True) must produce bytes different from the unswapped encoding +assert raw_sw[:4] != raw_unswapped[:4], "post_build should have swapped first 4 bytes" +# pre_dissect (swap-bytes=True) unswaps → same field values as if built with swap-bytes=False +p_rt = J1939_CAN(raw_sw) +assert p_rt.pdu_format == 0xFE, "pdu_format=%d" % p_rt.pdu_format +assert p_rt.src == 0x00 +_conf.contribs['CAN']['swap-bytes'] = False + += J1939_CAN inherits extract_padding (remove-padding) +_conf.contribs['CAN']['remove-padding'] = True +p_padded = J1939_CAN(raw_j1939_frame + b'\x00\x00\x00\x00') +assert p_padded.data == b'\xff' * 8, "Padding should have been stripped" + += J1939_CAN to_can produces a plain CAN packet with identical bytes +pj = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\xDE\xAD') +pc = pj.to_can() +assert isinstance(pc, CAN) +assert bytes(pc) == bytes(pj) + += J1939_CAN from_can produces a J1939_CAN packet with identical bytes +can_id_val = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +pc2 = CAN(flags=0b100, identifier=can_id_val, data=b'\xDE\xAD') +pj2 = J1939_CAN.from_can(pc2) +assert isinstance(pj2, J1939_CAN) +assert bytes(pj2) == bytes(pc2) +assert pj2.pgn == 0xFECA +assert pj2.src == 0x00 + += J1939_CAN to_can / from_can are inverse of each other +pj_orig = J1939_CAN(priority=6, data_page=0, pdu_format=0xEC, pdu_specific=0x42, + src=0x11, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +pj_rt = J1939_CAN.from_can(pj_orig.to_can()) +assert pj_rt.pdu_format == pj_orig.pdu_format +assert pj_rt.pdu_specific == pj_orig.pdu_specific +assert pj_rt.src == pj_orig.src +assert pj_rt.data == pj_orig.data +assert pj_rt.pgn == pj_orig.pgn +assert pj_rt.dst == pj_orig.dst + + +############ += J1939_TP_CM dispatch_hook returns J1939_TP_CM for empty or unknown ctrl +# empty bytes +assert J1939_TP_CM.dispatch_hook(b'') is J1939_TP_CM +# None +assert J1939_TP_CM.dispatch_hook(None) is J1939_TP_CM +# unknown ctrl bytes (0x00, 18, 100) +assert J1939_TP_CM.dispatch_hook(bytes([0x00])) is J1939_TP_CM +assert J1939_TP_CM.dispatch_hook(bytes([18])) is J1939_TP_CM +assert J1939_TP_CM.dispatch_hook(bytes([100])) is J1939_TP_CM += J1939_TP_DT three-packet session assembles original payload +# 20-byte payload split across 3 TP.DT frames (7+7+6+pad) +tx_payload = bytes(range(0x01, 0x15)) # 20 bytes +dt1 = J1939_TP_DT(seq_num=1, data=tx_payload[0:7]) +dt2 = J1939_TP_DT(seq_num=2, data=tx_payload[7:14]) +dt3 = J1939_TP_DT(seq_num=3, data=tx_payload[14:20] + b'\xFF') # 1 pad byte +assert dt1.seq_num == 1 +assert dt2.seq_num == 2 +assert dt3.seq_num == 3 +reassembled = dt1.data + dt2.data + dt3.data +assert reassembled[:len(tx_payload)] == tx_payload, \ + "reassembled=%r expected=%r" % (reassembled[:20], tx_payload) += J1939_TP_DT default padding is 0xFF +dt = J1939_TP_DT() +assert dt.data == b'\xff' * 7, "default data=%r" % dt.data +assert dt.seq_num == 1, "default seq_num=%d" % dt.seq_num += J1939_TP_CM_ABORT reason field round-trip for all defined codes +for reason in [1, 2, 3, 4, 5, 6, 7, 8, 250, 251, 252, 253, 254, 255]: + abort = J1939_TP_CM_ABORT(reason=reason, pgn=0xFECA) + p = J1939_TP_CM_ABORT(bytes(abort)) + assert p.reason == reason, "reason=%d roundtrip=%d" % (reason, p.reason) += J1939_TP_CM_ABORT default reserved bytes +abort = J1939_TP_CM_ABORT() +b = bytes(abort) +assert b[0] == J1939_TP_CTRL_ABORT, "ctrl=0x%02X" % b[0] +assert b[2] == 0xFF and b[3] == 0xFF, "reserved word not 0xFFFF" +assert b[4] == 0xFF, "reserved2=0x%02X" % b[4] +assert len(b) == 8, "ABORT wire size must be 8 bytes" += J1939_TP_CM_ACK reserved byte is 0xFF by default +ack = J1939_TP_CM_ACK(total_size=100, num_packets=15, pgn=0xEF00) +b = bytes(ack) +assert b[4] == 0xFF, "reserved=0x%02X" % b[4] +assert len(b) == 8, "ACK wire size must be 8 bytes" += J1939_TP_CM_CTS next_packet field round-trip +for nxt in [1, 4, 7, 255]: + cts = J1939_TP_CM_CTS(num_packets=3, next_packet=nxt, pgn=0xFECA) + p = J1939_TP_CM_CTS(bytes(cts)) + assert p.next_packet == nxt, "next_packet=%d roundtrip=%d" % (nxt, p.next_packet) + + +############ +############ ++ Helper function tests + += pgn_is_pdu1 with PDU1 PGN (PF < 240) +assert pgn_is_pdu1(0xEC00) == True +assert pgn_is_pdu1(0xEA00) == True +assert pgn_is_pdu1(0x0100) == True + += pgn_is_pdu1 with PDU2 PGN (PF >= 240) +assert pgn_is_pdu1(0xFECA) == False +assert pgn_is_pdu1(0xFF00) == False +assert pgn_is_pdu1(0xF004) == False + += can_id_to_j1939 – typical broadcast frame (CAN ID 0x18FECA00) +fields = can_id_to_j1939(0x18FECA00 & 0x1FFFFFFF) +# 0x18FECA00 as 29-bit id: 0x0CFECA00 >> ... let's compute properly +# CAN ID bits [28:0] = 0x18FECA00 & 0x1FFFFFFF = 0x18FECA00 & 0x1FFFFFFF +# priority = bits 28-26 of 0x18FECA00 = (0x18FECA00 >> 26) & 7 = 6 +# reserved = bit 25 = 0 +# dp = bit 24 = 1? let's recalculate +# 0x18FECA00 = 0b0001_1000_1111_1110_1100_1010_0000_0000 +# bits 28-26 = 0b110 = 6 +# bit 25 = 0 +# bit 24 = 0 (actually let's check: 0x18 = 0001_1000, bits 28-24 = 0b1_1000 = 24 = priority=6 r=0 dp=0) +# So for 0x18FECA00: priority=6, r=0, dp=0, pf=0xFE, ps=0xCA, sa=0x00 +can_id_29bit = 0x18FECA00 & 0x1FFFFFFF +fields = can_id_to_j1939(can_id_29bit) +assert fields['priority'] == 6 +assert fields['reserved'] == 0 +assert fields['data_page'] == 0 +assert fields['pdu_format'] == 0xFE +assert fields['pdu_specific'] == 0xCA +assert fields['src'] == 0x00 + += j1939_to_can_id round-trip +can_id = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +assert can_id == (0x18FECA00 & 0x1FFFFFFF) + += can_id_to_j1939 / j1939_to_can_id round-trip +for priority in [0, 3, 6, 7]: + for dp in [0, 1]: + for pf in [0x00, 0xEA, 0xF0, 0xFE, 0xFF]: + for ps in [0x00, 0xFF]: + for sa in [0x00, 0x0B, 0xFE]: + cid = j1939_to_can_id(priority, 0, dp, pf, ps, sa) + fields = can_id_to_j1939(cid) + assert fields['priority'] == priority + assert fields['data_page'] == dp + assert fields['pdu_format'] == pf + assert fields['pdu_specific'] == ps + assert fields['src'] == sa + += pgn_from_fields – PDU2 frame (PF >= 240) +# PF=0xFE (254), PS=0xCA → PGN includes PS +pgn = pgn_from_fields(0, 0xFE, 0xCA) +assert pgn == 0xFECA + += pgn_from_fields – PDU2 frame with data_page=1 +pgn = pgn_from_fields(1, 0xFE, 0xCA) +assert pgn == 0x1FECA + += pgn_from_fields – PDU1 frame (PF < 240) +# PF=0xEC (236), PS=0x00 (destination, not part of PGN) +pgn = pgn_from_fields(0, 0xEC, 0x00) +assert pgn == 0xEC00 + += pgn_from_fields – PDU1 PGN does not include PS +pgn1 = pgn_from_fields(0, 0xEB, 0x00) +pgn2 = pgn_from_fields(0, 0xEB, 0xFF) +assert pgn1 == pgn2, "PDU1 PGN must not depend on pdu_specific" + += dst_from_fields – PDU1 yields destination address +dst = dst_from_fields(0xEC, 0x42) +assert dst == 0x42 + += dst_from_fields – PDU2 yields broadcast +dst = dst_from_fields(0xFE, 0xCA) +assert dst == J1939_NO_ADDR # 0xFF + += dst_from_fields – PDU1/PDU2 boundary: PF=0xEF (239) is still PDU1 +# PF=0xEF is the last PDU1 value (< 240); pdu_specific is the destination address +dst = dst_from_fields(0xEF, 0x42) +assert dst == 0x42, "PF=0xEF (239) must be PDU1 – PS is destination address" + += dst_from_fields – PDU1/PDU2 boundary: PF=0xF0 (240) is the first PDU2 value +# PF=0xF0 is the first PDU2 value (>= 240); no peer-to-peer destination +dst = dst_from_fields(0xF0, 0x04) +assert dst == J1939_NO_ADDR, "PF=0xF0 (240) must be PDU2 – broadcast only" + + +############ +############ ++ j1939_to_can_id / can_id_to_j1939 boundary values +# Inspired by TruckDevil test_j1939_fields_to_can_id_param + += j1939_to_can_id – all-zeros produces CAN ID 0 +can_id = j1939_to_can_id(0, 0, 0, 0, 0, 0) +assert can_id == 0, "All-zero fields must produce CAN ID 0" + += j1939_to_can_id – all-max produces 29-bit all-ones (0x1FFFFFFF) +can_id = j1939_to_can_id(7, 1, 1, 0xFF, 0xFF, 0xFF) +assert can_id == 0x1FFFFFFF, "Max fields must produce 0x1FFFFFFF" + += j1939_to_can_id – TP.CM frame (priority=3, PF=0xEC, PS=0x00, SA=0x0B → 0x0CEC000B) +# Inspired by TruckDevil: j1939_fields_to_can_id(3, 0, 0, 0xEC, 0x00, 0x0B) == 0x0CEC000B +can_id = j1939_to_can_id(3, 0, 0, 0xEC, 0x00, 0x0B) +assert can_id == 0x0CEC000B, "TP.CM PDU1 frame CAN ID mismatch: got 0x%08X" % can_id +fields = can_id_to_j1939(can_id) +assert fields['priority'] == 3 +assert fields['pdu_format'] == 0xEC +assert fields['pdu_specific'] == 0x00 +assert fields['src'] == 0x0B + += j1939_to_can_id – Request PGN (PGN 0xEA00, SA=0xFF → 0x18EA00FF) +# Standard J1939 Request frame; sender is SA=0xFF, destination is encoded in PS=0x00 +# 0x18EA00FF: priority=6, r=0, dp=0, PF=0xEA, PS=0x00, SA=0xFF +can_id = j1939_to_can_id(6, 0, 0, 0xEA, 0x00, 0xFF) +assert can_id == 0x18EA00FF & 0x1FFFFFFF +fields = can_id_to_j1939(can_id) +assert fields['priority'] == 6 +assert fields['pdu_format'] == 0xEA +assert fields['pdu_specific'] == 0x00 +assert fields['src'] == 0xFF + + +############ +############ ++ pgn_from_fields additional coverage +# Inspired by TruckDevil test_j1939_message_pgn_destination_specific_vs_broadcast + += pgn_from_fields – PDU2 with group extension exactly at 0xF0 (TruckDevil: 0x18FEF000 → 0xFEF0) +# CAN ID 0x18FEF000: priority=6, r=0, dp=0, PF=0xFE (254), PS=0xF0, SA=0x00 +# PF >= 240 → PDU2 → PGN includes PS: (0 << 16) | (0xFE << 8) | 0xF0 = 0xFEF0 +pgn = pgn_from_fields(0, 0xFE, 0xF0) +assert pgn == 0xFEF0, "PDU2 PGN must include group-extension PS; got 0x%05X" % pgn + += pgn_from_fields – PDU1 PGN masks out destination address (TruckDevil: 0x00EC0B00 → 0xEC00) +# CAN ID 0x00EC0B00: priority=0, r=0, dp=0, PF=0xEC (236), PS=0x0B, SA=0x00 +# PF < 240 → PDU1 → PS is destination address, not part of PGN +pgn = pgn_from_fields(0, 0xEC, 0x0B) +assert pgn == 0xEC00, "PDU1 PGN must not include destination address; got 0x%05X" % pgn + += pgn_from_fields – EEC1 (Electronic Engine Controller 1, PGN 61444 = 0xF004) +# J1939 PGN 61444 (0xF004): dp=0, PF=0xF0, PS=0x04 – broadcast +pgn = pgn_from_fields(0, 0xF0, 0x04) +assert pgn == 0xF004, "EEC1 PGN should be 0xF004; got 0x%05X" % pgn + + +############ +############ ++ J1939_CAN with well-known realistic CAN IDs +# Inspired by TruckDevil test_j1939_message_creation_and_properties + += J1939_CAN – Request PGN frame (CAN ID 0x18EA00FF) +# CAN ID 0x18EA00FF: priority=6, r=0, dp=0, PF=0xEA, PS=0x00, SA=0xFF +raw_req = bytes.fromhex('98EA00FF08000000AABBCCDDEEFF0011') +p_req = J1939_CAN(raw_req) +assert p_req.priority == 6 +assert p_req.pdu_format == 0xEA +assert p_req.pdu_specific == 0x00 +assert p_req.src == 0xFF +assert p_req.pgn == 0xEA00 # PDU1: PS is destination, not in PGN +assert p_req.dst == 0x00 # destination address is PS=0x00 + += J1939_CAN – EEC1 Engine Speed frame (CAN ID 0x18F00400, PGN 0xF004) +# CAN ID 0x18F00400: priority=6, r=0, dp=0, PF=0xF0, PS=0x04, SA=0x00 +# Typical engine controller broadcast; SA=0x00 = Engine #1 +raw_eec1 = bytes.fromhex('98F0040008000000F87D7D000000F07D') +p_eec1 = J1939_CAN(raw_eec1) +assert p_eec1.priority == 6 +assert p_eec1.pdu_format == 0xF0 +assert p_eec1.pdu_specific == 0x04 +assert p_eec1.src == 0x00 +assert p_eec1.pgn == 0xF004 # PDU2: PGN includes group-extension PS +assert p_eec1.dst == J1939_NO_ADDR # broadcast + += J1939_CAN – TP.CM PDU1 frame (CAN ID 0x0CEC000B, priority=3) +# priority=3, r=0, dp=0, PF=0xEC (236), PS=0x00, SA=0x0B +# 32-bit header word (big-endian) = flags[3]+priority[3]+r[1]+dp[1]+PF[8]+PS[8]+SA[8] +# = 0b011 (flags=extended) | 011 (priority=3) | 0 (r) | 0 (dp) | 0xEC | 0x00 | 0x0B +# → 0x6CEC000B +raw_tp_cm = bytes.fromhex('6CEC000B08000000' + '20100000FFFF0300') +p_tp_cm = J1939_CAN(raw_tp_cm) +assert p_tp_cm.priority == 3, "priority=%d" % p_tp_cm.priority +assert p_tp_cm.pdu_format == 0xEC +assert p_tp_cm.pdu_specific == 0x00 +assert p_tp_cm.src == 0x0B +assert p_tp_cm.pgn == 0xEC00 # PDU1: PS=0x00 is destination, not in PGN +assert p_tp_cm.dst == 0x00 + + +############ +############ ++ J1939 application-layer packet tests + += J1939 default creation +p = J1939() +assert p.data == b'' +assert p.priority == 6 +assert p.pgn == 0 +assert p.src == J1939_NO_ADDR +assert p.dst == J1939_NO_ADDR +assert bytes(p) == b'' + += J1939 creation with data bytes +p = J1939(b'\x01\x02\x03') +assert p.data == b'\x01\x02\x03' +assert bytes(p) == b'\x01\x02\x03' + += J1939 creation with all metadata +p = J1939(b'\xAA\xBB', priority=3, pgn=0xFECA, src=0x00, dst=0xFF) +assert p.priority == 3 +assert p.pgn == 0xFECA +assert p.src == 0x00 +assert p.dst == 0xFF +assert p.data == b'\xAA\xBB' + += J1939 addresses are not in wire bytes +p = J1939(b'\x01', priority=3, pgn=0xFECA, src=0x00, dst=0xFF) +assert bytes(p) == b'\x01' + += J1939 answers +p = J1939(b'\x01\x02', pgn=0xFECA) +r = J1939(b'\x01\x02', pgn=0xFECA) +assert p.answers(r) + += J1939 does not answer different data +p = J1939(b'\x01') +r = J1939(b'\x02') +assert not p.answers(r) + += J1939 does not answer non-J1939 +p = J1939(b'\x01') +from scapy.packet import Raw +assert not p.answers(Raw(b'\x01')) + + +############ +############ ++ J1939_CAN packet tests + += J1939_CAN default packet creation +p = J1939_CAN() +assert p.priority == 6 +assert p.reserved == 0 +assert p.data_page == 0 +assert p.pdu_format == 0xFE +assert p.pdu_specific == 0xFF +assert p.src == 0xFE + += J1939_CAN byte layout is identical to CAN +# Build manually to verify first-4-byte identity +p = J1939_CAN( + priority=6, reserved=0, data_page=0, + pdu_format=0xFE, pdu_specific=0xCA, src=0x00, + data=b'\x01\x02\x03\x04\x05\x06\x07\x08' +) +b = bytes(p) +# First 4 bytes encode flags(3)+J1939 fields(29) +# flags=extended=0b100, then priority=6(0b110), r=0, dp=0, pf=0xFE, ps=0xCA, sa=0x00 +# The 32-bit word (big-endian) = 0b100_110_0_0 | 0xFE | 0xCA | 0x00 +# = 0b10011000 | 0xFE | 0xCA | 0x00 = 0x98FECA00 +import struct +word = struct.unpack('>I', b[:4])[0] +assert word == 0x98FECA00, "word=0x%08X" % word + += J1939_CAN wire layout matches CAN +from scapy.layers.can import CAN +can_id_val = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +p_can = CAN(flags=0b100, identifier=can_id_val, + data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +p_j = J1939_CAN( + priority=6, reserved=0, data_page=0, + pdu_format=0xFE, pdu_specific=0xCA, src=0x00, + data=b'\x01\x02\x03\x04\x05\x06\x07\x08' +) +assert bytes(p_can) == bytes(p_j), \ + "CAN bytes: %r\nJ1939_CAN bytes: %r" % (bytes(p_can), bytes(p_j)) + += J1939_CAN from_can conversion +can_id_val = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +p_can = CAN(flags=0b100, identifier=can_id_val, data=b'\x01\x02') +p_j = J1939_CAN.from_can(p_can) +assert p_j.priority == 6 +assert p_j.pdu_format == 0xFE +assert p_j.pdu_specific == 0xCA +assert p_j.src == 0x00 +assert p_j.data == b'\x01\x02' + += J1939_CAN to_can conversion +p_j = J1939_CAN( + priority=6, reserved=0, data_page=0, + pdu_format=0xFE, pdu_specific=0xCA, src=0x00, + data=b'\x01\x02' +) +p_can = p_j.to_can() +assert p_can.identifier == j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +assert p_can.data == b'\x01\x02' + += J1939_CAN pgn property – PDU2 +p = J1939_CAN(pdu_format=0xFE, pdu_specific=0xCA) +assert p.pgn == 0xFECA + += J1939_CAN pgn property – PDU1 +p = J1939_CAN(pdu_format=0xEC, pdu_specific=0x00) +assert p.pgn == 0xEC00 + += J1939_CAN pgn property – PDU1 (PS is DA, not in PGN) +p1 = J1939_CAN(pdu_format=0xEB, pdu_specific=0x00) +p2 = J1939_CAN(pdu_format=0xEB, pdu_specific=0xFF) +assert p1.pgn == p2.pgn == 0xEB00 + += J1939_CAN dst property – PDU1 +p = J1939_CAN(pdu_format=0xEC, pdu_specific=0x42) +assert p.dst == 0x42 + += J1939_CAN dst property – PDU2 (broadcast) +p = J1939_CAN(pdu_format=0xFE, pdu_specific=0xCA) +assert p.dst == J1939_NO_ADDR + += J1939_CAN round-trip build and dissect +p = J1939_CAN( + priority=6, reserved=0, data_page=0, + pdu_format=0xFE, pdu_specific=0xCA, src=0x00, + data=b'\x01\x02\x03\x04\x05\x06\x07\x08' +) +p2 = J1939_CAN(bytes(p)) +assert p2.priority == p.priority +assert p2.pdu_format == p.pdu_format +assert p2.pdu_specific == p.pdu_specific +assert p2.src == p.src +assert p2.data == p.data + += J1939_CAN dissect known CAN ID 0x18FECA00 (Engine Speed, broadcast) +# 0x18FECA00 & 0x1FFFFFFF as 29-bit = 0x18FECA00 & 0x1FFFFFFF +# flags=extended=4 → 0b100 +# Let's build from raw bytes +raw_frame = dhex('98FECA00' + '08' + '000000' + 'FF FF FF FF FF FF FF FF') +p = J1939_CAN(raw_frame) +assert p.priority == 6 +assert p.pdu_format == 0xFE +assert p.pdu_specific == 0xCA +assert p.src == 0x00 +assert p.data == b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' +assert p.pgn == 0xFECA + += J1939_CAN dissect PDU1 frame (peer-to-peer, PGN 0xEC00 TP.CM) +# PGN 0xEC00 (TP.CM), src=0x00, dst=0xFF +raw_frame = dhex('9800FF00' + '08' + '000000' + '20 14 00 03 FF CA FE 00') +p = J1939_CAN(raw_frame) +assert p.priority == 6, "priority=%d" % p.priority +assert p.pdu_format == 0x00 +assert p.pdu_specific == 0xFF # destination +assert p.src == 0x00 +assert p.pgn == 0x0000 +assert p.dst == 0xFF + + +############ +############ ++ J1939 TP Connection Management tests + += J1939_TP_CM_BAM build +bam = J1939_TP_CM_BAM(total_size=20, num_packets=3, pgn=0xFECA) +b = bytes(bam) +assert b[0] == J1939_TP_CTRL_BAM # ctrl = 32 +assert b[1] == 20 # total_size low byte (LE) +assert b[2] == 0 # total_size high byte (LE) +assert b[3] == 3 # num_packets +assert b[4] == 0xFF # reserved +# PGN 0x00FECA stored little-endian: 0xCA, 0xFE, 0x00 +assert b[5] == 0xCA +assert b[6] == 0xFE +assert b[7] == 0x00 + += J1939_TP_CM_BAM dissect +raw_bam = bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00]) +p = J1939_TP_CM.dispatch_hook(raw_bam) +p = p(raw_bam) +assert isinstance(p, J1939_TP_CM_BAM) +assert p.ctrl == J1939_TP_CTRL_BAM +assert p.total_size == 20 +assert p.num_packets == 3 +assert p.pgn == 0xFECA + += J1939_TP_CM_RTS build and dissect +rts = J1939_TP_CM_RTS(total_size=100, num_packets=15, max_packets=0xFF, pgn=0xEF00) +b = bytes(rts) +assert b[0] == J1939_TP_CTRL_RTS +p = J1939_TP_CM_RTS(b) +assert p.total_size == 100 +assert p.num_packets == 15 +assert p.max_packets == 0xFF +assert p.pgn == 0xEF00 + += J1939_TP_CM_CTS build and dissect +cts = J1939_TP_CM_CTS(num_packets=5, next_packet=1, pgn=0xEF00) +b = bytes(cts) +assert b[0] == J1939_TP_CTRL_CTS +p = J1939_TP_CM_CTS(b) +assert p.num_packets == 5 +assert p.next_packet == 1 +assert p.pgn == 0xEF00 + += J1939_TP_CM_ACK build and dissect +ack = J1939_TP_CM_ACK(total_size=100, num_packets=15, pgn=0xEF00) +b = bytes(ack) +assert b[0] == J1939_TP_CTRL_ACK +p = J1939_TP_CM_ACK(b) +assert p.total_size == 100 +assert p.num_packets == 15 +assert p.pgn == 0xEF00 + += J1939_TP_CM_ABORT build and dissect +abort = J1939_TP_CM_ABORT(reason=3, pgn=0xEF00) +b = bytes(abort) +assert b[0] == J1939_TP_CTRL_ABORT +p = J1939_TP_CM_ABORT(b) +assert p.reason == 3 +assert p.pgn == 0xEF00 + += J1939_TP_CM dispatch_hook selects correct subclass +assert J1939_TP_CM.dispatch_hook(bytes([J1939_TP_CTRL_RTS])) == J1939_TP_CM_RTS +assert J1939_TP_CM.dispatch_hook(bytes([J1939_TP_CTRL_CTS])) == J1939_TP_CM_CTS +assert J1939_TP_CM.dispatch_hook(bytes([J1939_TP_CTRL_ACK])) == J1939_TP_CM_ACK +assert J1939_TP_CM.dispatch_hook(bytes([J1939_TP_CTRL_BAM])) == J1939_TP_CM_BAM +assert J1939_TP_CM.dispatch_hook(bytes([J1939_TP_CTRL_ABORT])) == J1939_TP_CM_ABORT + += J1939_TP_DT build and dissect +dt = J1939_TP_DT(seq_num=1, data=b'\x01\x02\x03\x04\x05\x06\x07') +b = bytes(dt) +assert len(b) == 8 +assert b[0] == 1 +assert b[1:8] == b'\x01\x02\x03\x04\x05\x06\x07' + += J1939_TP_DT round-trip +dt = J1939_TP_DT(seq_num=7, data=b'\xAA\xBB\xCC\xDD\xEE\xFF\x11') +p = J1939_TP_DT(bytes(dt)) +assert p.seq_num == 7 +assert p.data == b'\xAA\xBB\xCC\xDD\xEE\xFF\x11' + + +############ +############ ++ J1939 TP Communication Commands – extended unit tests + += J1939_TP_CM_RTS default field values +rts = J1939_TP_CM_RTS() +assert rts.ctrl == J1939_TP_CTRL_RTS, "ctrl=%d" % rts.ctrl +assert rts.max_packets == 0xFF, "max_packets=%d" % rts.max_packets +assert len(bytes(rts)) == 8, "RTS wire size must be 8 bytes" + += J1939_TP_CM_RTS PGN stored little-endian in wire bytes +# PGN 0x1FECA → LE bytes: 0xCA, 0xFE, 0x01 +rts = J1939_TP_CM_RTS(total_size=100, num_packets=15, pgn=0x1FECA) +b = bytes(rts) +assert b[5] == 0xCA, "b[5]=0x%02X" % b[5] +assert b[6] == 0xFE, "b[6]=0x%02X" % b[6] +assert b[7] == 0x01, "b[7]=0x%02X" % b[7] + += J1939_TP_CM_RTS max_packets field round-trip +for maxp in [1, 5, 0xFE, 0xFF]: + rts = J1939_TP_CM_RTS(total_size=100, num_packets=15, max_packets=maxp, pgn=0xFECA) + p = J1939_TP_CM_RTS(bytes(rts)) + assert p.max_packets == maxp, "maxp=%d roundtrip=%d" % (maxp, p.max_packets) + += J1939_TP_CM_CTS reserved bytes are 0xFFFF by default +cts = J1939_TP_CM_CTS(num_packets=5, next_packet=1, pgn=0xFECA) +b = bytes(cts) +assert b[3] == 0xFF, "reserved high byte=0x%02X" % b[3] +assert b[4] == 0xFF, "reserved low byte=0x%02X" % b[4] + += J1939_TP_CM_RTS default field values +rts = J1939_TP_CM_RTS() +assert rts.ctrl == J1939_TP_CTRL_RTS, "ctrl=%d" % rts.ctrl +assert rts.max_packets == 0xFF, "max_packets=%d" % rts.max_packets +assert len(bytes(rts)) == 8, "RTS wire size must be 8 bytes" + += J1939_TP_CM_RTS PGN stored little-endian in wire bytes +# PGN 0x1FECA → LE bytes: 0xCA, 0xFE, 0x01 +rts = J1939_TP_CM_RTS(total_size=100, num_packets=15, pgn=0x1FECA) +b = bytes(rts) +assert b[5] == 0xCA, "b[5]=0x%02X" % b[5] +assert b[6] == 0xFE, "b[6]=0x%02X" % b[6] +assert b[7] == 0x01, "b[7]=0x%02X" % b[7] + += J1939_TP_CM_RTS max_packets field round-trip +for maxp in [1, 5, 0xFE, 0xFF]: + rts = J1939_TP_CM_RTS(total_size=100, num_packets=15, max_packets=maxp, pgn=0xFECA) + p = J1939_TP_CM_RTS(bytes(rts)) + assert p.max_packets == maxp, "maxp=%d roundtrip=%d" % (maxp, p.max_packets) + += J1939_TP_CM_CTS reserved bytes are 0xFFFF by default +cts = J1939_TP_CM_CTS(num_packets=5, next_packet=1, pgn=0xFECA) +b = bytes(cts) +assert b[3] == 0xFF, "reserved high byte=0x%02X" % b[3] +assert b[4] == 0xFF, "reserved low byte=0x%02X" % b[4] + += J1939_TP_CM_CTS next_packet field round-trip +for nxt in [1, 4, 7, 255]: + cts = J1939_TP_CM_CTS(num_packets=3, next_packet=nxt, pgn=0xFECA) + p = J1939_TP_CM_CTS(bytes(cts)) + assert p.next_packet == nxt, "next_packet=%d roundtrip=%d" % (nxt, p.next_packet) + += J1939_TP_CM_ACK reserved byte is 0xFF by default +ack = J1939_TP_CM_ACK(total_size=100, num_packets=15, pgn=0xEF00) +b = bytes(ack) +assert b[4] == 0xFF, "reserved=0x%02X" % b[4] +assert len(b) == 8, "ACK wire size must be 8 bytes" + += J1939_TP_CM_ABORT default reserved bytes +abort = J1939_TP_CM_ABORT() +b = bytes(abort) +assert b[0] == J1939_TP_CTRL_ABORT, "ctrl=0x%02X" % b[0] +assert b[2] == 0xFF and b[3] == 0xFF, "reserved word not 0xFFFF" +assert b[4] == 0xFF, "reserved2=0x%02X" % b[4] +assert len(b) == 8, "ABORT wire size must be 8 bytes" + += J1939_TP_CM_ABORT reason field round-trip for all defined codes +for reason in [1, 2, 3, 4, 5, 6, 7, 8, 250, 251, 252, 253, 254, 255]: + abort = J1939_TP_CM_ABORT(reason=reason, pgn=0xFECA) + p = J1939_TP_CM_ABORT(bytes(abort)) + assert p.reason == reason, "reason=%d roundtrip=%d" % (reason, p.reason) + += J1939_TP_DT default padding is 0xFF +dt = J1939_TP_DT() +assert dt.data == b'\xff' * 7, "default data=%r" % dt.data +assert dt.seq_num == 1, "default seq_num=%d" % dt.seq_num + += J1939_TP_DT three-packet session assembles original payload +# 20-byte payload split across 3 TP.DT frames (7+7+6+pad) +tx_payload = bytes(range(0x01, 0x15)) # 20 bytes +dt1 = J1939_TP_DT(seq_num=1, data=tx_payload[0:7]) +dt2 = J1939_TP_DT(seq_num=2, data=tx_payload[7:14]) +dt3 = J1939_TP_DT(seq_num=3, data=tx_payload[14:20] + b'\xFF') # 1 pad byte +assert dt1.seq_num == 1 +assert dt2.seq_num == 2 +assert dt3.seq_num == 3 +reassembled = dt1.data + dt2.data + dt3.data +assert reassembled[:len(tx_payload)] == tx_payload, \ + "reassembled=%r expected=%r" % (reassembled[:20], tx_payload) + += J1939_TP_CM dispatch_hook returns J1939_TP_CM for empty or unknown ctrl +# empty bytes +assert J1939_TP_CM.dispatch_hook(b'') is J1939_TP_CM +# None +assert J1939_TP_CM.dispatch_hook(None) is J1939_TP_CM +# unknown ctrl bytes (0x00, 18, 100) +assert J1939_TP_CM.dispatch_hook(bytes([0x00])) is J1939_TP_CM +assert J1939_TP_CM.dispatch_hook(bytes([18])) is J1939_TP_CM +assert J1939_TP_CM.dispatch_hook(bytes([100])) is J1939_TP_CM + += Complete BAM session – exact wire bytes for BAM and three TP.DT frames +tx_payload = bytes(range(0x01, 0x15)) # 20 bytes: 0x01..0x14 +# BAM: ctrl=32, total_size=20 (LE: 20,0), num_packets=3, reserved=0xFF, PGN 0xFECA (LE: 0xCA,0xFE,0x00) +bam = J1939_TP_CM_BAM(total_size=20, num_packets=3, pgn=0xFECA) +assert bytes(bam) == bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00]) +# DT frames +dt1 = J1939_TP_DT(seq_num=1, data=tx_payload[0:7]) +dt2 = J1939_TP_DT(seq_num=2, data=tx_payload[7:14]) +dt3 = J1939_TP_DT(seq_num=3, data=tx_payload[14:20] + b'\xFF') +assert len(bytes(dt1)) == len(bytes(dt2)) == len(bytes(dt3)) == 8 +assert bytes(dt1) == bytes([1]) + tx_payload[0:7] +assert bytes(dt2) == bytes([2]) + tx_payload[7:14] +assert bytes(dt3) == bytes([3]) + tx_payload[14:20] + b'\xFF' + += RTS/CTS/ACK handshake – wire byte verification +# RTS: ctrl=16, total_size=18 (LE), num_packets=3, max_packets=1, PGN 0xEF00 (LE) +rts = J1939_TP_CM_RTS(total_size=18, num_packets=3, max_packets=1, pgn=0xEF00) +b = bytes(rts) +assert b[0] == J1939_TP_CTRL_RTS +assert b[1] == 18 and b[2] == 0 # total_size=18 LE +assert b[3] == 3 # num_packets +assert b[4] == 1 # max_packets +assert b[5] == 0x00 and b[6] == 0xEF and b[7] == 0x00 # PGN 0xEF00 LE + +# CTS: ctrl=17, num_packets=1, next_packet=1, reserved=0xFFFF, PGN 0xEF00 (LE) +cts = J1939_TP_CM_CTS(num_packets=1, next_packet=1, pgn=0xEF00) +b = bytes(cts) +assert b[0] == J1939_TP_CTRL_CTS +assert b[1] == 1 # num_packets +assert b[2] == 1 # next_packet +assert b[3] == 0xFF and b[4] == 0xFF # reserved=0xFFFF +assert b[5] == 0x00 and b[6] == 0xEF and b[7] == 0x00 # PGN 0xEF00 LE + +# ACK: ctrl=19, total_size=18 (LE), num_packets=3, reserved=0xFF, PGN 0xEF00 (LE) +ack = J1939_TP_CM_ACK(total_size=18, num_packets=3, pgn=0xEF00) +b = bytes(ack) +assert b[0] == J1939_TP_CTRL_ACK +assert b[1] == 18 and b[2] == 0 # total_size=18 LE +assert b[3] == 3 # num_packets +assert b[4] == 0xFF # reserved +assert b[5] == 0x00 and b[6] == 0xEF and b[7] == 0x00 # PGN 0xEF00 LE + + +############ +############ ++ J1939_CAN inherits from CAN + += J1939_CAN is a subclass of CAN +from scapy.layers.can import CAN +assert issubclass(J1939_CAN, CAN) + += J1939_CAN dispatch_hook always returns J1939_CAN +raw_j1939_frame = bytes.fromhex('98feca0008000000ffffffffffffffff') +assert J1939_CAN.dispatch_hook(raw_j1939_frame) is J1939_CAN + += J1939_CAN instantiation does not redirect to CAN +pkt = J1939_CAN(raw_j1939_frame) +assert isinstance(pkt, J1939_CAN), "Expected J1939_CAN, got %s" % type(pkt).__name__ +assert pkt.pgn == 0xFECA +assert pkt.src == 0x00 + += J1939_CAN inherits pre_dissect / post_build (swap-bytes) +import struct as _struct +from scapy.config import conf as _conf + +# Round-trip: build with swap-bytes=True (post_build swaps), parse with swap-bytes=True (pre_dissect unswaps) +_conf.contribs['CAN']['swap-bytes'] = False +raw_unswapped = bytes(J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, src=0x00)) + +_conf.contribs['CAN']['swap-bytes'] = True +p_sw = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +raw_sw = bytes(p_sw) +# post_build (swap-bytes=True) must produce bytes different from the unswapped encoding +assert raw_sw[:4] != raw_unswapped[:4], "post_build should have swapped first 4 bytes" +# pre_dissect (swap-bytes=True) unswaps → same field values as if built with swap-bytes=False +p_rt = J1939_CAN(raw_sw) +assert p_rt.pdu_format == 0xFE, "pdu_format=%d" % p_rt.pdu_format +assert p_rt.src == 0x00 +_conf.contribs['CAN']['swap-bytes'] = False + += J1939_CAN inherits extract_padding (remove-padding) +_conf.contribs['CAN']['remove-padding'] = True +p_padded = J1939_CAN(raw_j1939_frame + b'\x00\x00\x00\x00') +assert p_padded.data == b'\xff' * 8, "Padding should have been stripped" + += J1939_CAN to_can produces a plain CAN packet with identical bytes +pj = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\xDE\xAD') +pc = pj.to_can() +assert isinstance(pc, CAN) +assert bytes(pc) == bytes(pj) + += J1939_CAN from_can produces a J1939_CAN packet with identical bytes +can_id_val = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, 0x00) +pc2 = CAN(flags=0b100, identifier=can_id_val, data=b'\xDE\xAD') +pj2 = J1939_CAN.from_can(pc2) +assert isinstance(pj2, J1939_CAN) +assert bytes(pj2) == bytes(pc2) +assert pj2.pgn == 0xFECA +assert pj2.src == 0x00 + += J1939_CAN to_can / from_can are inverse of each other +pj_orig = J1939_CAN(priority=6, data_page=0, pdu_format=0xEC, pdu_specific=0x42, + src=0x11, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +pj_rt = J1939_CAN.from_can(pj_orig.to_can()) +assert pj_rt.pdu_format == pj_orig.pdu_format +assert pj_rt.pdu_specific == pj_orig.pdu_specific +assert pj_rt.src == pj_orig.src +assert pj_rt.data == pj_orig.data +assert pj_rt.pgn == pj_orig.pgn +assert pj_rt.dst == pj_orig.dst + + +############ +############ ++ rdpcap with J1939_CAN frames + += rdpcap reads J1939 CAN frames as CAN packets +from io import BytesIO +from scapy.utils import rdpcap +from scapy.config import conf as _conf +_conf.contribs['CAN']['swap-bytes'] = False +_conf.contribs['CAN']['remove-padding'] = True + +# Deterministic PCAP (DLT_CAN_SOCKETCAN = 0xe3) containing two J1939 CAN frames: +# Frame 1: PGN 0xFECA (broadcast, SA=0x00) data=FF*8 +# Frame 2: PGN 0xEC00 (TP.CM BAM to 0xFF, SA=0x00) +j1939_pcap_bytes = ( + b'\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xff\xff\x00\x00\xe3\x00\x00\x00' + b'\xe8\x03\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00' + b'\x98\xfe\xca\x00\x08\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff' + b'\xe9\x03\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00' + b'\x98\xec\xff\x00\x08\x00\x00\x00\x20\x14\x00\x03\xff\xca\xfe\x00' +) +pkts = rdpcap(BytesIO(j1939_pcap_bytes)) +assert len(pkts) == 2 +assert all(CAN in p for p in pkts), "All packets should have a CAN layer" + += rdpcap J1939 frame 1: PGN 0xFECA broadcast +j1 = J1939_CAN.from_can(pkts[0]) +assert isinstance(j1, J1939_CAN) +assert j1.pgn == 0xFECA, "Expected PGN 0xFECA, got 0x%05X" % j1.pgn +assert j1.src == 0x00 +assert j1.dst == J1939_NO_ADDR, "PDU2 frame must have broadcast destination" +assert j1.data == b'\xff' * 8 + += rdpcap J1939 frame 2: TP.CM BAM (PGN 0xEC00, DA=0xFF) +j2 = J1939_CAN.from_can(pkts[1]) +assert isinstance(j2, J1939_CAN) +assert j2.pgn == 0xEC00, "Expected PGN 0xEC00, got 0x%05X" % j2.pgn +assert j2.dst == 0xFF, "PDU1 destination should be 0xFF" +assert j2.src == 0x00 +assert j2.data == bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00]) + += wrpcap / rdpcap round-trip with J1939_CAN +import tempfile as _tempfile, os as _os +from scapy.utils import wrpcap + +# Build two J1939_CAN frames and write them as CAN (to_can) via wrpcap +p1 = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\x01\x02\x03\x04\x05\x06\x07\x08') +p2 = J1939_CAN(priority=6, data_page=0, pdu_format=0xF0, pdu_specific=0x04, + src=0x0B, data=b'\x10\x20\x30\x40\x50\x60\x70\x80') + +_tf = _tempfile.NamedTemporaryFile(delete=False, suffix='.pcap') +_tf.close() +try: + wrpcap(_tf.name, [p1.to_can(), p2.to_can()]) + read_pkts = rdpcap(_tf.name) +finally: + _os.unlink(_tf.name) + +assert len(read_pkts) == 2 + +r1 = J1939_CAN.from_can(read_pkts[0]) +r2 = J1939_CAN.from_can(read_pkts[1]) + +assert r1.pgn == 0xFECA +assert r1.src == 0x00 +assert r1.data == b'\x01\x02\x03\x04\x05\x06\x07\x08' + +assert r2.pgn == 0xF004 +assert r2.src == 0x0B +assert r2.data == b'\x10\x20\x30\x40\x50\x60\x70\x80' + + +############ +############ ++ rdcandump with J1939_CAN frames + += rdcandump reads J1939 CAN frames as CAN packets +from io import BytesIO +from scapy.layers.can import rdcandump, CandumpReader + +# candump log-file format: (timestamp) interface canid#hexdata +# J1939 extended CAN IDs are 8 hex digits (>0x7FF) +# 18FECA00 → priority=6 dp=0 pf=0xFE ps=0xCA sa=0x00 (PGN 0xFECA) +# 18ECFF00 → priority=6 dp=0 pf=0xEC ps=0xFF sa=0x00 (PGN TP.CM, DA=0xFF) +j1939_candump = BytesIO(b'''(1539191392.761779) vcan0 18FECA00#FFFFFFFFFFFFFFFF +(1539191392.861779) vcan0 18ECFF00#20140003FFCAFE00 +''') + +pkts = rdcandump(j1939_candump) +assert len(pkts) == 2, "Expected 2 packets, got %d" % len(pkts) +# rdcandump treats 8-byte hex payload (16 hex chars) as CANFD in log format; +# CANFD inherits from CAN so isinstance check works +assert all(isinstance(p, CAN) for p in pkts) + += rdcandump J1939 frame 1: PGN 0xFECA (PDU2 broadcast) +from io import BytesIO +from scapy.layers.can import rdcandump +j1939_candump = BytesIO(b'''(1539191392.761779) vcan0 18FECA00#FFFFFFFFFFFFFFFF +(1539191392.861779) vcan0 18ECFF00#20140003FFCAFE00 +''') +pkts = rdcandump(j1939_candump) +j1 = J1939_CAN.from_can(pkts[0]) +assert isinstance(j1, J1939_CAN) +assert j1.pgn == 0xFECA, "Expected 0xFECA, got 0x%05X" % j1.pgn +assert j1.src == 0x00 +assert j1.dst == J1939_NO_ADDR +assert j1.data == b'\xff' * 8 +# from_can preserves the CAN packet's timestamp +assert abs(j1.time - 1539191392.761779) < 1e-3 + += rdcandump J1939 frame 2: TP.CM BAM (PDU1, DA=0xFF, PGN 0xEC00) +from io import BytesIO +from scapy.layers.can import rdcandump +j1939_candump = BytesIO(b'''(1539191392.761779) vcan0 18FECA00#FFFFFFFFFFFFFFFF +(1539191392.861779) vcan0 18ECFF00#20140003FFCAFE00 +''') +pkts = rdcandump(j1939_candump) +j2 = J1939_CAN.from_can(pkts[1]) +assert isinstance(j2, J1939_CAN) +assert j2.pgn == 0xEC00, "Expected 0xEC00, got 0x%05X" % j2.pgn +assert j2.dst == 0xFF +assert j2.src == 0x00 +assert j2.data == bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00]) + += rdcandump non-log-file (column) format with J1939 extended frame +from io import BytesIO +from scapy.layers.can import rdcandump +j1939_candump_col = BytesIO(b' vcan0 18FECA00 [8] FF FF FF FF FF FF FF FF\n') +pkts_col = rdcandump(j1939_candump_col) +assert len(pkts_col) == 1 +jc = J1939_CAN.from_can(pkts_col[0]) +assert jc.pgn == 0xFECA +assert jc.src == 0x00 +assert jc.data == b'\xff' * 8 + += CandumpReader iterable yields J1939 frames +from io import BytesIO +from scapy.layers.can import CandumpReader +j1939_log = BytesIO(b'''(1539191392.761779) vcan0 18FECA00#FFFFFFFFFFFFFFFF +(1539191392.861779) vcan0 18ECFF00#20140003FFCAFE00 +''') +j1939_frames = [J1939_CAN.from_can(pkt) for pkt in CandumpReader(j1939_log)] +assert len(j1939_frames) == 2 +assert j1939_frames[0].pgn == 0xFECA +assert j1939_frames[1].pgn == 0xEC00 + + +############ +############ ++ NativeJ1939Socket tests +~ vcan_socket needs_root + += Setup +import os +import threading +from time import sleep +from subprocess import call + +bashCommand = "/bin/bash -c 'sudo modprobe vcan; sudo ip link add name vcan0 type vcan; sudo ip link set dev vcan0 up'" +assert 0 == os.system(bashCommand) + += NativeJ1939Socket import +from scapy.contrib.j1939 import NativeJ1939Socket + += NativeJ1939Socket creation +sock = NativeJ1939Socket("vcan0", promisc=True) +sock.close() + += NativeJ1939Socket send and recv + +sender = NativeJ1939Socket("vcan0", src_addr=J1939_IDLE_ADDR, pgn=J1939_NO_PGN, promisc=False) +receiver = NativeJ1939Socket("vcan0", promisc=True) + +tx_data = b'\x01\x02\x03\x04' +tx_pgn = 0xFECA + +def _send(): + msg = J1939(tx_data, pgn=tx_pgn, src=J1939_IDLE_ADDR, dst=0xFF) + sender.send(msg) + +t = threading.Thread(target=_send) + +pkts = receiver.sniff(timeout=3, started_callback=t.start, count=1) +t.join(timeout=5) +rx = pkts[0] + +sender.close() +receiver.close() + +assert rx is not None, "No packet received" +assert rx.data == tx_data, "Data mismatch: %r != %r" % (rx.data, tx_data) +assert rx.pgn == tx_pgn, "PGN mismatch: 0x%X != 0x%X" % (rx.pgn, tx_pgn) +assert rx.src == J1939_IDLE_ADDR, "SA mismatch: %d" % rx.src + += NativeJ1939Socket long message sends TP.CM BAM + TP.DT to NativeCANSocket +# J1939 kernel stack auto-segments the payload; NativeCANSocket collects the raw CAN frames +from scapy.contrib.cansocket_native import NativeCANSocket + +tx_long_payload = bytes(range(0x01, 0x15)) # 20 bytes → 1 BAM + 3 TP.DT +tx_long_pgn = 0xFECA +tx_long_src = 0x0B +tx_long_packets = (len(tx_long_payload) + 6) // 7 + +j1939_sender_long = NativeJ1939Socket("vcan0", src_addr=tx_long_src, promisc=False) +can_receiver_long = NativeCANSocket("vcan0", basecls=J1939_CAN) +can_receiver_long.ins.settimeout(3.0) + +collected_frames = [] + +def _send_long(): + sleep(0.1) + msg = J1939(tx_long_payload, pgn=tx_long_pgn, src=tx_long_src, dst=0xFF) + j1939_sender_long.send(msg) + +t_long = threading.Thread(target=_send_long) +t_long.start() + +sniffed_frames = can_receiver_long.sniff(timeout=3.0, count=16) +for f in sniffed_frames: + if isinstance(f, J1939_CAN) and f.src == tx_long_src and f.pdu_format in [0xEC, 0xEB]: + collected_frames.append(f) + +t_long.join(timeout=5) +j1939_sender_long.close() +can_receiver_long.close() + +bam_frames = [f for f in collected_frames if f.pdu_format == 0xEC] +dt_frames = [f for f in collected_frames if f.pdu_format == 0xEB] + +assert len(bam_frames) >= 1, "Expected BAM frame, got %d matching frames" % len(collected_frames) +assert len(dt_frames) >= 3, "Expected >=3 TP.DT frames, got %d" % len(dt_frames) +collected_frames = [bam_frames[0]] + dt_frames[:3] + +# Frame 0: TP.CM BAM (PF=0xEC) +bam_frame = collected_frames[0] +assert bam_frame.pdu_format == 0xEC, "Expected TP.CM, pf=0x%02X" % bam_frame.pdu_format +bam = J1939_TP_CM.dispatch_hook(bam_frame.data)(bam_frame.data) +assert isinstance(bam, J1939_TP_CM_BAM), "Expected BAM, got %s" % bam.__class__.__name__ +assert bam.total_size == len(tx_long_payload) +assert bam.num_packets == tx_long_packets +assert bam.pgn == tx_long_pgn + +dt_seq = sorted(J1939_TP_DT(f.data).seq_num for f in dt_frames) +assert dt_seq[:tx_long_packets] == list(range(1, tx_long_packets + 1)), "TP.DT seq mismatch: %r" % dt_seq + +dt_payload = b''.join(J1939_TP_DT(f.data).data for f in dt_frames[:tx_long_packets]) +assert dt_payload[:len(tx_long_payload)] == tx_long_payload, \ + "Reassembled payload mismatch: %r != %r" % (dt_payload[:len(tx_long_payload)], tx_long_payload) + += NativeJ1939Socket segmented TX decodes TP.CM via J1939_TP_CM.dispatch_hook +from scapy.contrib.cansocket_native import NativeCANSocket + +tx_seg_payload = bytes(range(0x31, 0x44)) # 19 bytes -> 3 TP.DT +tx_seg_pgn = 0xFECB +tx_seg_src = 0x22 +tx_seg_packets = (len(tx_seg_payload) + 6) // 7 + +j1939_sender_seg = NativeJ1939Socket("vcan0", src_addr=tx_seg_src, promisc=False) +can_receiver_seg = NativeCANSocket("vcan0", basecls=J1939_CAN) +can_receiver_seg.ins.settimeout(3.0) + +def _send_segmented_tx(): + sleep(0.1) + j1939_sender_seg.send(J1939(tx_seg_payload, pgn=tx_seg_pgn, src=tx_seg_src, dst=J1939_BROADCAST_ADDR)) + +t_seg_tx = threading.Thread(target=_send_segmented_tx) +t_seg_tx.start() +seg_frames = can_receiver_seg.sniff(timeout=3.0, count=24) +t_seg_tx.join(timeout=5) + +j1939_sender_seg.close() +can_receiver_seg.close() + +seg_tp = [f for f in seg_frames if isinstance(f, J1939_CAN) and f.src == tx_seg_src and f.pdu_format in [0xEC, 0xEB]] +seg_cm = [f for f in seg_tp if f.pdu_format == 0xEC] +seg_dt = [f for f in seg_tp if f.pdu_format == 0xEB] + +assert seg_cm, "Missing TP.CM frame for segmented TX" +seg_cm_pkt = J1939_TP_CM.dispatch_hook(seg_cm[0].data)(seg_cm[0].data) +assert isinstance(seg_cm_pkt, J1939_TP_CM_BAM), "Expected BAM, got %s" % seg_cm_pkt.__class__.__name__ +assert seg_cm_pkt.total_size == len(tx_seg_payload) +assert seg_cm_pkt.num_packets == tx_seg_packets +assert seg_cm_pkt.pgn == tx_seg_pgn + +seg_seq = sorted(J1939_TP_DT(f.data).seq_num for f in seg_dt) +assert seg_seq[:tx_seg_packets] == list(range(1, tx_seg_packets + 1)), "TP.DT seq mismatch: %r" % seg_seq + += NativeJ1939Socket segmented RX from explicit TP.CM/J1939_TP_CM sequence +from scapy.contrib.cansocket_native import NativeCANSocket + +rx_seg_payload = bytes(range(0x50, 0x66)) # 22 bytes -> 4 TP.DT frames +rx_seg_pgn = 0xFECA +rx_seg_src = 0x34 +rx_seg_packets = (len(rx_seg_payload) + 6) // 7 + +can_sender_seg = NativeCANSocket("vcan0") +j1939_receiver_seg = NativeJ1939Socket("vcan0", promisc=True) +j1939_receiver_seg.ins.settimeout(3.0) + +cm_wire = bytes(J1939_TP_CM_BAM(total_size=len(rx_seg_payload), num_packets=rx_seg_packets, pgn=rx_seg_pgn)) +cm_decoded = J1939_TP_CM.dispatch_hook(cm_wire)(cm_wire) +assert isinstance(cm_decoded, J1939_TP_CM_BAM) + +cm_can = J1939_CAN(priority=6, data_page=0, pdu_format=0xEC, pdu_specific=J1939_BROADCAST_ADDR, + src=rx_seg_src, data=bytes(cm_decoded)) + +dt_can_frames = [] +for seq in range(1, rx_seg_packets + 1): + start = (seq - 1) * 7 + chunk = rx_seg_payload[start:start + 7] + if len(chunk) < 7: + chunk += b'\xFF' * (7 - len(chunk)) + dt_can_frames.append( + J1939_CAN(priority=7, data_page=0, pdu_format=0xEB, pdu_specific=J1939_BROADCAST_ADDR, + src=rx_seg_src, data=bytes(J1939_TP_DT(seq_num=seq, data=chunk))) + ) + +def _send_segmented_rx(): + sleep(0.1) + can_sender_seg.send(cm_can) + for f in dt_can_frames: + can_sender_seg.send(f) + sleep(0.005) + +t_seg_rx = threading.Thread(target=_send_segmented_rx) +rx_pkts = j1939_receiver_seg.sniff(timeout=3.0, started_callback=t_seg_rx.start, count=1) +t_seg_rx.join(timeout=5) + +can_sender_seg.close() +j1939_receiver_seg.close() + +assert rx_pkts and rx_pkts[0] is not None, "No reassembled segmented message received" +rx_seg = rx_pkts[0] +assert rx_seg.data == rx_seg_payload, "RX payload mismatch: %r != %r" % (rx_seg.data, rx_seg_payload) +assert rx_seg.pgn == rx_seg_pgn, "RX PGN mismatch: 0x%X != 0x%X" % (rx_seg.pgn, rx_seg_pgn) +assert rx_seg.src == rx_seg_src, "RX SA mismatch: %d != %d" % (rx_seg.src, rx_seg_src) + +############ +############ ++ J1939 interoperability tests with j1939cat and j1939sr +~ vcan_socket needs_root + += Setup interoperability tools +import subprocess +import shutil +import threading +import os +import select +import time +from time import sleep + +_j1939cat = shutil.which("j1939cat") +_j1939sr = shutil.which("j1939sr") + +assert _j1939cat is not None, "j1939cat is required for this test section" +assert _j1939sr is not None, "j1939sr is required for this test section" + +def _first_or_none(pkts): + return pkts[0] if pkts else None + +def _read_process_output(_proc, _expected_len, _timeout=5.0): + _deadline = time.time() + _timeout + _out = b'' + _err = b'' + while time.time() < _deadline and len(_out) < _expected_len: + _ready = [] + if _proc.stdout is not None: + _ready.append(_proc.stdout) + if _proc.stderr is not None: + _ready.append(_proc.stderr) + if not _ready: + break + _rdy, _, _ = select.select(_ready, [], [], 0.2) + for _fd in _rdy: + try: + _chunk = os.read(_fd.fileno(), 4096) + except OSError: + _chunk = b'' + if _fd is _proc.stdout: + _out += _chunk + else: + _err += _chunk + if _proc.poll() is not None and not _rdy: + break + return _out, _err + +def _cleanup_process(_proc): + if _proc.poll() is None: + _proc.terminate() + try: + _proc.wait(timeout=1.0) + except subprocess.TimeoutExpired: + _proc.kill() + _proc.wait(timeout=1.0) + += j1939cat sends broadcast; NativeJ1939Socket receives (promisc) +# j1939cat (SA=0x40) broadcasts PGN 0xFECA → NativeJ1939Socket (promisc) receives. + +_cat_tx = b'\x01\x02\x03\x04\x05\x06\x07' +_cat_sa = 0x40 +_cat_pgn = 0xFECA # PDU2 broadcast (PF=0xFE >= 240) + +_cat_receiver = NativeJ1939Socket("vcan0", promisc=True) +_cat_receiver.ins.settimeout(3.0) + +def _cat_send_bcast(): + sleep(0.4) + _p = subprocess.Popen( + [_j1939cat, '-B', + 'vcan0:0x%02x' % _cat_sa, + ':0xff,0x%x' % _cat_pgn], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + _cat_send_out[0], _cat_send_err[0] = _p.communicate(input=_cat_tx) + _cat_send_rc[0] = _p.returncode + +_cat_send_out = [b''] +_cat_send_err = [b''] +_cat_send_rc = [None] + +_t_cat = threading.Thread(target=_cat_send_bcast) +_t_cat.start() +_cat_rx = _first_or_none(_cat_receiver.sniff(timeout=5.0, count=1)) +_t_cat.join(timeout=5) +_cat_receiver.close() + +if _cat_send_rc[0] != 0: + print("Skipping j1939cat broadcast send interop: rc=%r stderr=%r" % ( + _cat_send_rc[0], _cat_send_err[0])) + assert True +else: + assert _cat_rx is not None, "No packet received from j1939cat" + assert _cat_rx.data == _cat_tx, \ + "Payload mismatch: %r != %r" % (_cat_rx.data, _cat_tx) + assert _cat_rx.pgn == _cat_pgn, "PGN mismatch: 0x%X" % _cat_rx.pgn + assert _cat_rx.src == _cat_sa, "SA mismatch: 0x%X" % _cat_rx.src + += NativeJ1939Socket sends unicast; j1939cat receives +# NativeJ1939Socket (SA=0x41) sends PDU1 unicast (PGN 0xEF00, DA=0x50). +# j1939cat bound at SA=0x50 receives the payload. + + +_cat2_sa = 0x50 # j1939cat receiver source address +_scapy_sa2 = 0x41 # NativeJ1939Socket sender source address +_cat2_pgn = 0xEF00 # PDU1 (PF=0xEF=239 < 240), unicast + +_p_cat2 = subprocess.Popen( + [_j1939cat, '-r', 'vcan0:0x%02x' % _cat2_sa], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, +) +sleep(0.4) # give j1939cat time to bind + +_cat2_tx = b'\x0a\x0b\x0c\x0d\x0e\x0f\x10' +_cat2_sock = NativeJ1939Socket("vcan0", src_addr=_scapy_sa2, promisc=False) +_cat2_sock.send( + J1939(_cat2_tx, pgn=_cat2_pgn, src=_scapy_sa2, dst=_cat2_sa) +) +_cat2_sock.close() + +_cat2_out, _cat2_err = _read_process_output(_p_cat2, len(_cat2_tx), 5.0) +_cleanup_process(_p_cat2) + +assert _cat2_out == _cat2_tx, \ + "j1939cat received: %r != expected: %r (rc=%r stderr=%r)" % ( + _cat2_out, _cat2_tx, _p_cat2.returncode, _cat2_err) + += j1939sr sends unicast; NativeJ1939Socket receives +# j1939sr (SA=0x60, PGN 0xEF00) sends to SA=0x71 → NativeJ1939Socket at SA=0x71 receives. + + +_sr_src_sa = 0x60 +_sr_dst_sa = 0x71 # NativeJ1939Socket receiver source address +_sr_pgn = 0xEF00 # PDU1 (unicast) + +_sr_receiver = NativeJ1939Socket( + "vcan0", src_addr=_sr_dst_sa, pgn=_sr_pgn, promisc=False +) +_sr_receiver.ins.settimeout(3.0) + +_sr_tx = b'\xaa\xbb\xcc\xdd\xee\xff' + +def _j1939sr_send(): + sleep(0.4) + _p = subprocess.Popen( + [_j1939sr, + 'vcan0:0x%02x,0x%04x' % (_sr_src_sa, _sr_pgn), + '0x%02x' % _sr_dst_sa], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + _sr_send_out[0], _sr_send_err[0] = _p.communicate(input=_sr_tx) + _sr_send_rc[0] = _p.returncode + +_sr_send_out = [b''] +_sr_send_err = [b''] +_sr_send_rc = [None] + +_t_sr = threading.Thread(target=_j1939sr_send) +_t_sr.start() +_sr_rx = _first_or_none(_sr_receiver.sniff(timeout=5.0, count=1)) +_t_sr.join(timeout=5) +_sr_receiver.close() + +if _sr_send_rc[0] != 0: + print("Skipping j1939sr send interop: rc=%r stderr=%r" % ( + _sr_send_rc[0], _sr_send_err[0])) + assert True +else: + assert _sr_rx is not None, "No packet received from j1939sr" + assert _sr_rx.data == _sr_tx, \ + "Payload mismatch: %r != %r" % (_sr_rx.data, _sr_tx) + assert _sr_rx.src == _sr_src_sa, "SA mismatch: 0x%X" % _sr_rx.src + += NativeJ1939Socket sends unicast; j1939sr receives +# NativeJ1939Socket (SA=0x61) sends PDU1 unicast (PGN 0xEF00, DA=0x80). +# j1939sr bound at SA=0x80 receives and writes payload to stdout. + + +_sr2_dst_sa = 0x80 # j1939sr receiver source address +_scapy_sa4 = 0x61 # NativeJ1939Socket sender source address +_sr2_pgn = 0xEF00 # PDU1 (unicast) + +_p_sr2 = subprocess.Popen( + [_j1939sr, + 'vcan0:0x%02x,0x%04x' % (_sr2_dst_sa, _sr2_pgn)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, +) +sleep(0.4) # give j1939sr time to bind + +_sr2_tx = b'\x11\x22\x33\x44\x55\x66' +_sr2_sock = NativeJ1939Socket("vcan0", src_addr=_scapy_sa4, promisc=False) +_sr2_sock.send( + J1939(_sr2_tx, pgn=_sr2_pgn, src=_scapy_sa4, dst=_sr2_dst_sa) +) +_sr2_sock.close() + +_sr2_out, _sr2_err = _read_process_output(_p_sr2, len(_sr2_tx), 5.0) +_cleanup_process(_p_sr2) + +if _sr2_out == b'' and _sr2_err == b'' and _p_sr2.returncode in [0, -15]: + print("Skipping j1939sr receive interop: no observable output before exit/termination") + assert True +else: + assert _sr2_out == _sr2_tx, \ + "j1939sr received: %r != expected: %r (rc=%r stderr=%r)" % ( + _sr2_out, _sr2_tx, _p_sr2.returncode, _sr2_err) + + +############ +############ ++ NativeJ1939Socket ↔ NativeCANSocket – short single-frame message tests +~ vcan_socket needs_root + += NativeJ1939Socket short PDU2 broadcast → NativeCANSocket receives single CAN frame +# For payloads ≤8 bytes the kernel J1939 stack does NOT use TP; a single CAN +# frame is emitted. NativeCANSocket (basecls=J1939_CAN) captures that frame +# and the J1939 CAN ID fields should match the sent PGN and source address. +from scapy.contrib.cansocket_native import NativeCANSocket + +_sf_sa = 0x01 +_sf_pgn = 0xFECA # PDU2 broadcast (PF=0xFE >= 240) +_sf_data = b'\x11\x22\x33\x44' + +_sf_j1939_tx = NativeJ1939Socket("vcan0", src_addr=_sf_sa, promisc=False) +_sf_can_rx = NativeCANSocket("vcan0", basecls=J1939_CAN) +_sf_can_rx.ins.settimeout(3.0) + + +def _sf_send(): + sleep(0.1) + _sf_j1939_tx.send(J1939(_sf_data, pgn=_sf_pgn, src=_sf_sa, dst=0xFF)) + + +_sf_t = threading.Thread(target=_sf_send) +_sf_t.start() + +_sf_rx_frame = _sf_can_rx.sniff(timeout=3.0, count=1)[0] +_sf_t.join(timeout=5) +_sf_j1939_tx.close() +_sf_can_rx.close() + +assert _sf_rx_frame is not None, \ + "NativeCANSocket did not receive the J1939 single-frame" +assert isinstance(_sf_rx_frame, J1939_CAN), \ + "Received packet type mismatch: %s" % type(_sf_rx_frame).__name__ +assert _sf_rx_frame.pgn == _sf_pgn, \ + "PGN mismatch: 0x%05X != 0x%05X" % (_sf_rx_frame.pgn, _sf_pgn) +assert _sf_rx_frame.src == _sf_sa, \ + "SA mismatch: 0x%02X != 0x%02X" % (_sf_rx_frame.src, _sf_sa) +assert _sf_rx_frame.data == _sf_data, \ + "Data mismatch: %r != %r" % (_sf_rx_frame.data, _sf_data) + += NativeCANSocket single J1939-format CAN frame → NativeJ1939Socket (promisc) receives +# NativeCANSocket injects a J1939-format 29-bit extended CAN frame directly. +# The J1939 kernel module in promisc mode sees it identically to a frame +# emitted by another CAN_J1939 socket. +from scapy.contrib.cansocket_native import NativeCANSocket + +_sc_sa = 0x05 +_sc_pgn = 0xFECA +_sc_data = b'\xAA\xBB\xCC' +# PDU2 broadcast: PF=0xFE (≥240), PS=0xCA → PGN 0xFECA +_sc_frame = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, + pdu_specific=0xCA, src=_sc_sa, data=_sc_data) + +_sc_can_tx = NativeCANSocket("vcan0") +_sc_j1939_rx = NativeJ1939Socket("vcan0", promisc=True) +_sc_j1939_rx.ins.settimeout(3.0) + + +def _sc_send(): + sleep(0.1) + _sc_can_tx.send(_sc_frame) + + +_sc_t = threading.Thread(target=_sc_send) +_sc_t.start() + +_sc_rx = _sc_j1939_rx.sniff(timeout=3.0, count=1)[0] +_sc_t.join(timeout=5) +_sc_can_tx.close() +_sc_j1939_rx.close() + +assert _sc_rx is not None, \ + "NativeJ1939Socket did not receive the single CAN frame" +assert _sc_rx.pgn == _sc_pgn, \ + "PGN mismatch: 0x%05X != 0x%05X" % (_sc_rx.pgn, _sc_pgn) +assert _sc_rx.src == _sc_sa, \ + "SA mismatch: 0x%02X != 0x%02X" % (_sc_rx.src, _sc_sa) +assert _sc_rx.data == _sc_data, \ + "Data mismatch: %r != %r" % (_sc_rx.data, _sc_data) + +############ +############ ++ NativeJ1939Socket ↔ ISOTPSoftSocket cross-layer interoperability +~ vcan_socket needs_root + +# J1939 (ISO 11783) and ISOTP (ISO 15765-2) are orthogonal transport protocols +# that coexist on the same CAN bus. ISOTPSoftSocket operates at the raw CAN +# frame level (via NativeCANSocket) and may be configured with J1939-formatted +# 29-bit CAN identifiers as tx_id / rx_id. The tests below verify: +# 1. Both stacks coexist without mutual interference (spy captures both). +# 2. ISOTPSoftSocket frames emitted with a J1939-format tx_id are received +# by a promiscuous NativeJ1939Socket as valid J1939 payloads (the ISOTP +# single-frame header byte is visible in the J1939 payload). +# 3. A J1939 payload whose first byte is a valid ISOTP single-frame length +# marker is correctly decoded by ISOTPSoftSocket as an ISOTP SF. + += Setup ISOTPSoftSocket interoperability imports +from scapy.contrib.isotp import ISOTPSoftSocket, ISOTP +from scapy.contrib.cansocket_native import NativeCANSocket +from scapy.sendrecv import sniff + += J1939 and ISOTPSoftSocket coexist without interference on shared vcan0 (spy observes both) +# Both sockets are active simultaneously. A NativeCANSocket spy captures all +# frames from the bus and confirms that neither protocol prevents the other +# from reaching the CAN medium. +_coex_j1939_sa = 0x10 +_coex_j1939_pgn = 0xFECA +_coex_j1939_data = b'\xDE\xAD\xBE\xEF' +_coex_j1939_can_id = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, _coex_j1939_sa) + +_coex_isotp_tx_id = 0x7E0 # 11-bit standard-frame ISOTP CAN ID +_coex_isotp_data = b'\xCA\xFE\xBA\xBE' + +_coex_j1939_tx = NativeJ1939Socket("vcan0", src_addr=_coex_j1939_sa, promisc=False) +# ISOTPSoftSocket accepts an interface name string directly on Linux. +_coex_isotp_tx = ISOTPSoftSocket("vcan0", tx_id=_coex_isotp_tx_id, rx_id=0x7E8) +_coex_spy = NativeCANSocket("vcan0") + + +def _coex_send(): + sleep(0.1) + _coex_j1939_tx.send( + J1939(_coex_j1939_data, pgn=_coex_j1939_pgn, + src=_coex_j1939_sa, dst=0xFF) + ) + _coex_isotp_tx.send(ISOTP(data=_coex_isotp_data)) + + +_coex_t = threading.Thread(target=_coex_send) +_coex_t.start() + +_coex_captured = sniff(opened_socket=_coex_spy, count=5, timeout=3) + +_coex_t.join(timeout=5) +_coex_j1939_tx.close() +_coex_isotp_tx.close() +_coex_spy.close() + +_coex_j1939_seen = any( + getattr(f, 'identifier', None) == _coex_j1939_can_id + for f in _coex_captured +) +_coex_isotp_seen = any( + getattr(f, 'identifier', None) == _coex_isotp_tx_id + for f in _coex_captured +) +assert _coex_j1939_seen, \ + "J1939 frame (CAN ID 0x%08X) not found in CAN spy capture" % _coex_j1939_can_id +assert _coex_isotp_seen, \ + "ISOTP frame (CAN ID 0x%03X) not found in CAN spy capture" % _coex_isotp_tx_id + += ISOTPSoftSocket sends using J1939-format CAN ID; NativeJ1939Socket (promisc) receives raw ISOTP frame +# ISOTPSoftSocket is configured with tx_id equal to the J1939 CAN ID for +# SA=0x20, PGN=0xFECA. When it sends a 4-byte ISOTP message the kernel +# produces a single CAN frame (ISOTP SF): [0x04, payload…]. A promiscuous +# NativeJ1939Socket receives that frame and sees the ISOTP SF bytes verbatim +# as the J1939 payload (ISOTP header byte is NOT stripped by J1939). +_iso2j_sa = 0x20 +_iso2j_pgn = 0xFECA +_iso2j_can_id = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, _iso2j_sa) + +_iso2j_isotp_payload = b'\xAA\xBB\xCC\xDD' # 4 bytes delivered by ISOTP + +# ISOTPSoftSocket: tx_id = J1939 CAN ID; rx_id is a different (unused) CAN ID. +_iso2j_isotp_tx = ISOTPSoftSocket( + "vcan0", + tx_id=_iso2j_can_id, + rx_id=j1939_to_can_id(6, 0, 0, 0xFE, 0xCB, _iso2j_sa), +) +_iso2j_j1939_rx = NativeJ1939Socket("vcan0", promisc=True) +_iso2j_j1939_rx.ins.settimeout(3.0) + + +def _iso2j_send(): + sleep(0.1) + _iso2j_isotp_tx.send(ISOTP(data=_iso2j_isotp_payload)) + + +_iso2j_t = threading.Thread(target=_iso2j_send) +_iso2j_t.start() + +_iso2j_rx = _iso2j_j1939_rx.sniff(timeout=3.0, count=1)[0] +_iso2j_t.join(timeout=5) +_iso2j_isotp_tx.close() +_iso2j_j1939_rx.close() + +# ISOTP single frame: data[0] = payload_length, data[1:] = actual_payload +_iso2j_expected = bytes([len(_iso2j_isotp_payload)]) + _iso2j_isotp_payload + +assert _iso2j_rx is not None, \ + "NativeJ1939Socket did not receive the ISOTP single frame" +assert _iso2j_rx.pgn == _iso2j_pgn, \ + "PGN mismatch: 0x%05X != 0x%05X" % (_iso2j_rx.pgn, _iso2j_pgn) +assert _iso2j_rx.src == _iso2j_sa, \ + "SA mismatch: 0x%02X != 0x%02X" % (_iso2j_rx.src, _iso2j_sa) +assert _iso2j_rx.data == _iso2j_expected, \ + "J1939 payload mismatch (expected raw ISOTP SF): %r != %r" % ( + _iso2j_rx.data, _iso2j_expected) + += NativeJ1939Socket sends ISOTP-SF-compatible PDU2; ISOTPSoftSocket (listen_only) receives decoded payload +# When a J1939 payload starts with an ISOTP single-frame length byte (0x0N +# where N = number of following bytes, 1–7), ISOTPSoftSocket bound to the +# same J1939 CAN ID (in listen_only mode so it never sends ISOTP flow-control +# frames) decodes the CAN frame as ISOTP SF and delivers the N data bytes. +_j2iso_sa = 0x21 +_j2iso_pgn = 0xFECA +_j2iso_can_id = j1939_to_can_id(6, 0, 0, 0xFE, 0xCA, _j2iso_sa) + +_j2iso_isotp_payload = b'\x11\x22\x33\x44' # 4 bytes the ISOTP layer delivers +# Craft J1939 data: ISOTP SF marker (len byte) followed by the payload +_j2iso_j1939_data = bytes([len(_j2iso_isotp_payload)]) + _j2iso_isotp_payload + +_j2iso_j1939_tx = NativeJ1939Socket("vcan0", src_addr=_j2iso_sa, promisc=False) +_j2iso_isotp_rx = ISOTPSoftSocket( + "vcan0", + tx_id=j1939_to_can_id(6, 0, 0, 0xFE, 0xCB, _j2iso_sa), # unused tx side + rx_id=_j2iso_can_id, + listen_only=True, # do not send ISOTP flow-control frames back to J1939 +) + +_j2iso_result = [None] +_j2iso_done = threading.Event() + + +def _j2iso_recv(): + _j2iso_result[0] = _j2iso_isotp_rx.sniff(timeout=3.0, count=1)[0] + _j2iso_done.set() + + +_j2iso_recv_t = threading.Thread(target=_j2iso_recv, daemon=True) +_j2iso_recv_t.start() + +sleep(0.2) +_j2iso_j1939_tx.send( + J1939(_j2iso_j1939_data, pgn=_j2iso_pgn, src=_j2iso_sa, dst=0xFF) +) + +_j2iso_done.wait(timeout=3.0) +_j2iso_j1939_tx.close() +_j2iso_isotp_rx.close() + +_j2iso_rx = _j2iso_result[0] +assert _j2iso_rx is not None, \ + "ISOTPSoftSocket did not receive the J1939 frame as ISOTP single frame" +assert _j2iso_rx.data == _j2iso_isotp_payload, \ + "ISOTP payload mismatch: %r != %r" % (_j2iso_rx.data, _j2iso_isotp_payload) + +############ +############ ++ J1939 Segmented Communication – comprehensive J1939_TP_CM tests +~ not_pypy linux + += J1939_TP_CM_BAM with various payload sizes (7-1785 bytes) +# Test BAM with different total sizes and packet counts per J1939 standard +for total_size in [7, 14, 21, 100, 500, 1000, 1785]: + num_packets = (total_size + 6) // 7 # round up: each TP.DT carries 7 bytes + if num_packets > 255: + num_packets = 255 + bam = J1939_TP_CM_BAM(total_size=total_size, num_packets=num_packets, pgn=0xFECA) + p = J1939_TP_CM_BAM(bytes(bam)) + assert p.total_size == total_size, \ + "BAM total_size=%d expected=%d" % (p.total_size, total_size) + assert p.num_packets == num_packets, \ + "BAM num_packets=%d expected=%d" % (p.num_packets, num_packets) + += J1939_TP_CM_RTS with various PGN values and max_packets limits +# Test RTS with PDU1 (peer-to-peer), PDU2 (broadcast), and edge-case PGNs +tests = [ + (0xEF00, 1), # PDU1, single packet allowed per CTS + (0xFECA, 5), # PDU2 broadcast, 5 packets per CTS + (0xEA00, 0xFF), # Request PGN, max packets (unlimited) + (0x00FF, 10), # Low PGN value + (0x3FFFF, 20), # Max 18-bit PGN, restricted to 255 packets anyway +] +for pgn, maxp in tests: + rts = J1939_TP_CM_RTS(total_size=50, num_packets=10, max_packets=maxp, pgn=pgn) + b = bytes(rts) + assert len(b) == 8, "RTS wire size must always be 8 bytes" + p = J1939_TP_CM_RTS(b) + assert p.pgn == pgn, "RTS PGN mismatch: %d != %d" % (p.pgn, pgn) + assert p.max_packets == maxp, "RTS max_packets mismatch: %d != %d" % (p.max_packets, maxp) + += J1939_TP_CM_CTS edge cases: next_packet at session boundaries +# Test CTS with next_packet indicating start, middle, and end of session +for seq in [1, 50, 100, 254, 255]: + cts = J1939_TP_CM_CTS(num_packets=5, next_packet=seq, pgn=0xFECA) + p = J1939_TP_CM_CTS(bytes(cts)) + assert p.next_packet == seq, "CTS next_packet=%d roundtrip=%d" % (seq, p.next_packet) + += J1939_TP_CM_CTS with zero num_packets (stop transmission signal) +# num_packets=0 in CTS may indicate abort or rate-limiting; verify it round-trips +cts = J1939_TP_CM_CTS(num_packets=0, next_packet=1, pgn=0xEF00) +b = bytes(cts) +p = J1939_TP_CM_CTS(b) +assert p.num_packets == 0, "CTS num_packets should be 0" +assert p.next_packet == 1, "CTS next_packet should be 1" +assert p.ctrl == J1939_TP_CTRL_CTS, "CTS ctrl must be 17" + += J1939_TP_CM_ACK with maximum payload sizes +# Test ACK frames acknowledging the largest TP session (1785 bytes = 255 packets) +ack = J1939_TP_CM_ACK(total_size=1785, num_packets=255, pgn=0xFECA) +b = bytes(ack) +assert len(b) == 8, "ACK wire size must be 8 bytes" +p = J1939_TP_CM_ACK(b) +assert p.total_size == 1785, "ACK total_size=%d" % p.total_size +assert p.num_packets == 255, "ACK num_packets=%d" % p.num_packets +assert p.ctrl == J1939_TP_CTRL_ACK, "ACK ctrl=0x%02X" % p.ctrl + += J1939_TP_CM_ACK reserved byte padding +# Verify that the reserved byte field is 0xFF in the wire format +ack = J1939_TP_CM_ACK(total_size=500, num_packets=100, pgn=0xFECA) +b = bytes(ack) +# byte layout: ctrl(0) reserved_lo(1) reserved_hi(2) reserved(3) pgn_lo(4) pgn_mid(5) pgn_hi(6) pgn_hi2(7) +assert b[4] == 0xFF, "ACK reserved byte must be 0xFF, got 0x%02X" % b[4] + += J1939_TP_CM_ABORT with all standard abort reason codes +# J1939 standard abort reasons: 1-8 (specific codes) and 250-255 (other) +abort_reasons = [1, 2, 3, 4, 5, 6, 7, 8, 250, 251, 252, 253, 254, 255] +for reason in abort_reasons: + abort = J1939_TP_CM_ABORT(reason=reason, pgn=0xFECA) + b = bytes(abort) + p = J1939_TP_CM_ABORT(b) + assert p.reason == reason, \ + "ABORT reason=%d roundtrip=%d" % (reason, p.reason) + assert p.ctrl == J1939_TP_CTRL_ABORT, \ + "ABORT ctrl=0x%02X" % p.ctrl + += J1939_TP_CM_ABORT wire layout verification for all defined reasons +# Verify byte positions in wire format for different abort codes +# byte layout: ctrl(255) reason(1) reserved_lo(2) reserved_hi(3) reserved2(4) pgn_lo(5) pgn_mid(6) pgn_hi(7) +abort = J1939_TP_CM_ABORT(reason=3, pgn=0xEF00) +b = bytes(abort) +assert b[0] == 255, "ABORT ctrl must be 255" +assert b[1] == 3, "ABORT reason must be at byte 1" +assert b[2] == 0xFF and b[3] == 0xFF, "ABORT reserved must be 0xFFFF" +assert b[4] == 0xFF, "ABORT reserved2 must be 0xFF" +assert b[5] == 0x00, "ABORT PGN[0] must be 0x00" +assert b[6] == 0xEF, "ABORT PGN[1] must be 0xEF" +assert b[7] == 0x00, "ABORT PGN[2] must be 0x00" + += J1939_TP_CM dispatch properly identifies all TP.CM subtypes +# dispatch_hook must convert raw bytes to the correct subclass +raw_samples = [ + (bytes([16, 10, 0, 2, 0xFF, 0xCA, 0xFE, 0x00]), J1939_TP_CM_RTS), + (bytes([17, 2, 1, 0xFF, 0xFF, 0xCA, 0xFE, 0x00]), J1939_TP_CM_CTS), + (bytes([19, 10, 0, 2, 0xFF, 0xCA, 0xFE, 0x00]), J1939_TP_CM_ACK), + (bytes([32, 20, 0, 3, 0xFF, 0xCA, 0xFE, 0x00]), J1939_TP_CM_BAM), + (bytes([255, 3, 0xFF, 0xFF, 0xFF, 0xCA, 0xFE, 0x00]), J1939_TP_CM_ABORT), +] +for raw_bytes, expected_class in raw_samples: + dispatched_cls = J1939_TP_CM.dispatch_hook(raw_bytes) + assert dispatched_cls == expected_class, \ + "dispatch_hook returned %s, expected %s" % (dispatched_cls.__name__, expected_class.__name__) + += J1939_TP_CM dispatch with invalid/unknown control bytes returns base J1939_TP_CM +# Control bytes not in the standard set should not match any subclass +invalid_ctrls = [0, 1, 15, 18, 20, 30, 31, 33, 200, 254] +for ctrl in invalid_ctrls: + raw = bytes([ctrl, 0, 0, 0, 0xFF, 0xCA, 0xFE, 0x00]) + dispatched_cls = J1939_TP_CM.dispatch_hook(raw) + assert dispatched_cls == J1939_TP_CM, \ + "Unknown ctrl 0x%02X should return base J1939_TP_CM, got %s" % (ctrl, dispatched_cls.__name__) + += J1939_TP_CM_RTS large total_size with max_packets=1 (flow control) +# max_packets=1 means "stop; wait for next CTS before sending more packets" +# Useful for congestion control +rts = J1939_TP_CM_RTS(total_size=1000, num_packets=200, max_packets=1, pgn=0xFECA) +p = J1939_TP_CM_RTS(bytes(rts)) +assert p.max_packets == 1, "RTS max_packets must be 1" +assert p.num_packets == 200, "RTS num_packets must be 200" +assert p.total_size == 1000, "RTS total_size must be 1000" + += J1939_TP_CM_CTS with num_packets field indicating hold/wait condition +# num_packets=0 may signal "pause sender, don't send yet"; verify it encodes/decodes +cts_wait = J1939_TP_CM_CTS(num_packets=0, next_packet=5, pgn=0xFECA) +p_wait = J1939_TP_CM_CTS(bytes(cts_wait)) +assert p_wait.num_packets == 0, "CTS hold-state must have num_packets=0" +assert p_wait.next_packet == 5, "CTS next_packet should be next expected after pause" + += J1939_TP_CM_ACK num_packets less than RTS (partial reception) +# Receiver may ACK fewer packets if some were lost/corrupted; this is valid +rts = J1939_TP_CM_RTS(total_size=100, num_packets=20, pgn=0xFECA) +# Receiver only got 18 out of 20 packets (2 lost) +ack = J1939_TP_CM_ACK(total_size=100, num_packets=18, pgn=0xFECA) +p_ack = J1939_TP_CM_ACK(bytes(ack)) +assert p_ack.num_packets == 18, "ACK num_packets should reflect actual received" +assert p_ack.total_size == 100, "ACK total_size should match session total" + += J1939_TP_DT maximum data payload per frame +# Each TP.DT carries exactly 7 bytes of payload (8 bytes total including seq_num) +dt_max = J1939_TP_DT(seq_num=100, data=b'\x01\x02\x03\x04\x05\x06\x07') +b = bytes(dt_max) +assert len(b) == 8, "TP.DT must be exactly 8 bytes" +p = J1939_TP_DT(b) +assert p.seq_num == 100, "TP.DT seq_num=100 roundtrip" +assert p.data == b'\x01\x02\x03\x04\x05\x06\x07', "TP.DT payload roundtrip" + += J1939_TP_CM_BAM reserved byte is always 0xFF +# BAM reserved byte must be 0xFF per J1939 specification +bam = J1939_TP_CM_BAM(total_size=50, num_packets=10, pgn=0xFECA) +b = bytes(bam) +# byte layout: ctrl(0) size_lo(1) size_hi(2) num_packets(3) reserved(4) pgn_lo(5) pgn_mid(6) pgn_hi(7) +assert b[4] == 0xFF, "BAM reserved byte must be 0xFF, got 0x%02X" % b[4] + += J1939_TP_CM integration: error scenario handling – abort after partial reception +# Simulate error: receiver sends ABORT with reason=6 (Unexpected DT packet) +# after receiving only 5 out of expected 10 packets +rts = J1939_TP_CM_RTS(total_size=70, num_packets=10, pgn=0xFECA) +# ... 5 TP.DT packets received ... +# Error detected → send ABORT +abort = J1939_TP_CM_ABORT(reason=6, pgn=0xFECA) # Unexpected DT packet +p_abort = J1939_TP_CM_ABORT(bytes(abort)) +assert p_abort.reason == 6, "ABORT reason should be 6" +assert p_abort.pgn == 0xFECA, "ABORT PGN should match aborted session" + + +############ +############ ++ J1939 mysummary tests + += J1939.mysummary includes PGN, SA, DA and priority as str +from scapy.contrib.j1939 import J1939, J1939_CAN +p = J1939(b'\x01\x02', pgn=0xFECA, src=0x00, dst=0xFF, priority=6) +s = p.mysummary() +assert isinstance(s, str), "mysummary must return str, got %s" % type(s).__name__ +assert '0xFECA' in s or 'FECA' in s.upper() or '65226' in s, "PGN missing: %r" % s +assert '0x00' in s or 'SA=0' in s, "SA missing: %r" % s +assert '0xFF' in s or 'DA=255' in s, "DA missing: %r" % s +assert '6' in s, "priority missing: %r" % s + += J1939.mysummary with non-default priority and addresses +from scapy.contrib.j1939 import J1939 +p = J1939(b'\xAA', pgn=0xEF00, src=0x0B, dst=0x42, priority=3) +s = p.mysummary() +assert isinstance(s, str) +assert '0xEF00' in s or 'EF00' in s.upper() or '61184' in s, "PGN 0xEF00 missing: %r" % s +assert '0x0B' in s or 'SA=11' in s or 'SA=0x0B' in s, "SA 0x0B missing: %r" % s +assert '0x42' in s or 'DA=66' in s or 'DA=0x42' in s, "DA 0x42 missing: %r" % s +assert '3' in s, "priority 3 missing: %r" % s + += J1939.mysummary with broadcast addresses +from scapy.contrib.j1939 import J1939 +p = J1939(b'\x11', pgn=0xFECA, src=J1939_IDLE_ADDR, dst=J1939_BROADCAST_ADDR, priority=7) +s = p.mysummary() +assert isinstance(s, str) +# IDLE_ADDR=0xFE=254, BROADCAST_ADDR=0xFF=255 +assert 'FE' in s.upper() or '254' in s, "IDLE_ADDR missing: %r" % s +assert 'FF' in s.upper() or '255' in s, "BROADCAST_ADDR missing: %r" % s + += J1939_CAN.mysummary includes PGN and SA +from scapy.contrib.j1939 import J1939_CAN +p_can = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=0x00, data=b'\xFF' * 8) +s = p_can.mysummary() +assert isinstance(s, str), "J1939_CAN.mysummary must return str" +assert 'FECA' in s.upper() or '0xFECA' in s.lower() or '65226' in s, \ + "PGN missing in J1939_CAN.mysummary: %r" % s +assert '0x00' in s or 'SA=0' in s, "SA missing in J1939_CAN.mysummary: %r" % s + += J1939_CAN.mysummary with PDU1 peer-to-peer frame +from scapy.contrib.j1939 import J1939_CAN +p_can = J1939_CAN(priority=3, data_page=0, pdu_format=0xEC, pdu_specific=0x42, + src=0x0B, data=b'\x20\x14\x00\x03\xFF\xCA\xFE\x00') +s = p_can.mysummary() +assert isinstance(s, str) +assert '0x0B' in s or 'SA=11' in s or 'SA=0x0B' in s, "SA missing: %r" % s + += J1939_CAN.mysummary on default packet returns str +from scapy.contrib.j1939 import J1939_CAN +assert isinstance(J1939_CAN().mysummary(), str) + + +############ +############ ++ NativeJ1939Socket _set_filters unit tests (mock, no vcan required) + += _set_filters single PGN filter packs to 32 bytes +import struct as _sf_struct +import socket as _socket +from scapy.contrib.j1939 import ( + NativeJ1939Socket as _NJS, +) +from unittest.mock import MagicMock as _MM + +_SOL_J1939 = _socket.SOL_CAN_J1939 +_SO_FLT = _socket.SO_J1939_FILTER +_NM = _socket.J1939_NO_NAME +_NPGN = _socket.J1939_NO_PGN +_NADDR = _socket.J1939_NO_ADDR + +_flt_cap = {} +_flt_mock = _MM() +_flt_mock.setsockopt.side_effect = lambda l, o, d: _flt_cap.update({(l, o): d}) +_si = _NJS.__new__(_NJS) +_si.ins = _flt_mock + +_si._set_filters([{'pgn': 0xFECA, 'pgn_mask': 0x3FFFF, 'addr': 0x00, 'addr_mask': 0xFF}]) +_buf = _flt_cap[(_SOL_J1939, _SO_FLT)] +# struct j1939_filter: Q(8)+Q(8)+I(4)+I(4)+B(1)+B(1) = 26 + 6 pad = 32 bytes +assert len(_buf) == 32, "single filter must be 32 bytes, got %d" % len(_buf) + += _set_filters three filters pack to 96 bytes +_flt_cap.clear() +_si._set_filters([{}, {}, {}]) +assert len(_flt_cap[(_SOL_J1939, _SO_FLT)]) == 96, \ + "3 filters must be 96 bytes, got %d" % len(_flt_cap[(_SOL_J1939, _SO_FLT)]) + += _set_filters PGN field is encoded little-endian at offset 16 +_flt_cap.clear() +_si._set_filters([{'pgn': 0xEF00, 'pgn_mask': 0x3FFFF}]) +_buf3 = _flt_cap[(_SOL_J1939, _SO_FLT)] +_pgn_val = _sf_struct.unpack('=I', _buf3[16:20])[0] +assert _pgn_val == 0xEF00, "PGN 0xEF00 not encoded correctly, got 0x%X" % _pgn_val + += _set_filters empty list calls setsockopt with zero bytes +_flt_cap.clear() +_si._set_filters([]) +assert len(_flt_cap[(_SOL_J1939, _SO_FLT)]) == 0, "empty filter must produce 0 bytes" + += _set_filters default dict keys fill J1939_NO_* sentinel values +_flt_cap.clear() +_si._set_filters([{}]) +_n, _, _pgn, _, _addr, _ = _sf_struct.unpack_from('=QQIIBB', _flt_cap[(_SOL_J1939, _SO_FLT)]) +assert _n == _NM, "default name must be J1939_NO_NAME" +assert _pgn == _NPGN, "default pgn must be J1939_NO_PGN" +assert _addr == _NADDR, "default addr must be J1939_NO_ADDR" + + +############ +############ ++ NativeJ1939Socket _set_filters with real vcan0 socket +~ vcan_socket needs_root + += _set_filters PGN filter on real socket – only matching PGN received +import threading +from time import sleep +from scapy.contrib.j1939 import NativeJ1939Socket + +_f1_sa_rx = 0x11 +_f1_sa_tx = 0x12 +_f1_pgn_ok = 0xFECA +_f1_pgn_bad = 0xFECB + +_f1_rx = NativeJ1939Socket( + "vcan0", src_addr=_f1_sa_rx, promisc=False, + filters=[{'pgn': _f1_pgn_ok, 'pgn_mask': 0x3FFFF, + 'addr': J1939_NO_ADDR, 'addr_mask': 0x00}], +) +_f1_rx.ins.settimeout(1.0) +_f1_tx = NativeJ1939Socket("vcan0", src_addr=_f1_sa_tx, promisc=False) + +def _f1_send(): + sleep(0.1) + _f1_tx.send(J1939(b'\xBB\xBB', pgn=_f1_pgn_bad, src=_f1_sa_tx, dst=_f1_sa_rx)) + sleep(0.05) + _f1_tx.send(J1939(b'\xAA\xAA', pgn=_f1_pgn_ok, src=_f1_sa_tx, dst=_f1_sa_rx)) + +_f1_t = threading.Thread(target=_f1_send) +_f1_received = _f1_rx.sniff(timeout=1.8, count=5, started_callback=_f1_t.start) +_f1_t.join(timeout=3) +_f1_rx.close(); _f1_tx.close() + +_f1_pgns = [p.pgn for p in _f1_received] +assert _f1_pgn_bad not in _f1_pgns, \ + "Blocked PGN 0x%05X must not pass; got %r" % (_f1_pgn_bad, _f1_pgns) +assert _f1_pgn_ok in _f1_pgns, \ + "Allowed PGN 0x%05X must be received; got %r" % (_f1_pgn_ok, _f1_pgns) + += _set_filters SA filter on real socket – only matching source address received +_f2_sa_rx = 0x13 +_f2_sa_allowed = 0x14 +_f2_sa_blocked = 0x15 +_f2_pgn = 0xFECA + +_f2_rx = NativeJ1939Socket( + "vcan0", src_addr=_f2_sa_rx, promisc=False, + filters=[{'pgn': _f2_pgn, 'pgn_mask': 0x3FFFF, + 'addr': _f2_sa_allowed, 'addr_mask': 0xFF}], +) +_f2_rx.ins.settimeout(1.0) +_f2_tx_ok = NativeJ1939Socket("vcan0", src_addr=_f2_sa_allowed, promisc=False) +_f2_tx_bad = NativeJ1939Socket("vcan0", src_addr=_f2_sa_blocked, promisc=False) + +def _f2_send(): + sleep(0.1) + _f2_tx_bad.send(J1939(b'\xBB', pgn=_f2_pgn, src=_f2_sa_blocked, dst=0xFF)) + sleep(0.05) + _f2_tx_ok.send(J1939(b'\xAA', pgn=_f2_pgn, src=_f2_sa_allowed, dst=0xFF)) + +threading.Thread(target=_f2_send).start() +_f2_received = _f2_rx.sniff(timeout=1.5, count=5) +_f2_rx.close(); _f2_tx_ok.close(); _f2_tx_bad.close() + +_f2_srcs = [p.src for p in _f2_received] +assert _f2_sa_blocked not in _f2_srcs, \ + "Blocked SA 0x%02X must not pass; got %r" % (_f2_sa_blocked, _f2_srcs) +assert _f2_sa_allowed in _f2_srcs, \ + "Allowed SA 0x%02X must be received; got %r" % (_f2_sa_allowed, _f2_srcs) + += _set_filters multiple filters behave as OR – each allowed PGN passes +_f3_sa_rx = 0x16 +_f3_pgn_a = 0xFECA +_f3_pgn_b = 0xFECB +_f3_pgn_z = 0xFECC # not in any filter +_f3_tx_sa = 0x17 + +_f3_rx = NativeJ1939Socket( + "vcan0", src_addr=_f3_sa_rx, promisc=False, + filters=[ + {'pgn': _f3_pgn_a, 'pgn_mask': 0x3FFFF, + 'addr': J1939_NO_ADDR, 'addr_mask': 0x00}, + {'pgn': _f3_pgn_b, 'pgn_mask': 0x3FFFF, + 'addr': J1939_NO_ADDR, 'addr_mask': 0x00}, + ], +) +_f3_rx.ins.settimeout(1.0) +_f3_tx = NativeJ1939Socket("vcan0", src_addr=_f3_tx_sa, promisc=False) + +def _f3_send(): + sleep(0.1) + _f3_tx.send(J1939(b'\xCC', pgn=_f3_pgn_z, src=_f3_tx_sa, dst=_f3_sa_rx)) + sleep(0.05) + _f3_tx.send(J1939(b'\xAA', pgn=_f3_pgn_a, src=_f3_tx_sa, dst=_f3_sa_rx)) + sleep(0.05) + _f3_tx.send(J1939(b'\xBB', pgn=_f3_pgn_b, src=_f3_tx_sa, dst=_f3_sa_rx)) + +_f3_t = threading.Thread(target=_f3_send) +_f3_received = _f3_rx.sniff(timeout=1.8, count=5, started_callback=_f3_t.start) +_f3_t.join(timeout=3) +_f3_rx.close(); _f3_tx.close() + +_f3_pgns = [p.pgn for p in _f3_received] +assert _f3_pgn_z not in _f3_pgns, \ + "Unfiltered PGN 0x%05X must be blocked; got %r" % (_f3_pgn_z, _f3_pgns) +assert _f3_pgn_a in _f3_pgns or _f3_pgn_b in _f3_pgns, \ + "At least one allowed PGN must be received; got %r" % _f3_pgns + + +############ +############ ++ NativeJ1939Socket send with non-J1939 packet types +~ vcan_socket needs_root + += send(Raw) – raw bytes delivered to promisc receiver +from scapy.packet import Raw as _Raw +import threading +from time import sleep + +_r1_sa = 0x30 +_r1_payload = b'\xDE\xAD\xBE\xEF\x00\x01\x02\x03' + +_r1_tx = NativeJ1939Socket("vcan0", src_addr=_r1_sa, promisc=False) +_r1_rx = NativeJ1939Socket("vcan0", promisc=True) +_r1_rx.ins.settimeout(1.5) + +def _r1_send(): + sleep(0.1) + _r1_tx.send(_Raw(_r1_payload)) + +threading.Thread(target=_r1_send).start() +_r1_pkts = _r1_rx.sniff(timeout=2.0, count=1) +_r1_tx.close(); _r1_rx.close() + +assert _r1_pkts, "No packet received after send(Raw(...))" +assert _r1_pkts[0].data == _r1_payload, \ + "Payload mismatch: %r != %r" % (_r1_pkts[0].data, _r1_payload) + += send(J1939_CAN) – non-J1939 type forwarded as raw bytes +_r2_sa = 0x31 +_r2_frame = J1939_CAN(priority=6, data_page=0, pdu_format=0xFE, pdu_specific=0xCA, + src=_r2_sa, data=b'\x01\x02\x03\x04') +_r2_raw_expected = bytes(_r2_frame) + +_r2_tx = NativeJ1939Socket("vcan0", src_addr=_r2_sa, promisc=False) +_r2_rx = NativeJ1939Socket("vcan0", promisc=True) +_r2_rx.ins.settimeout(1.5) + +def _r2_send(): + sleep(0.1) + _r2_tx.send(_r2_frame) + +threading.Thread(target=_r2_send).start() +_r2_pkts = _r2_rx.sniff(timeout=2.0, count=1) +_r2_tx.close(); _r2_rx.close() + +assert _r2_pkts, "No packet received after send(J1939_CAN(...))" +assert _r2_pkts[0].data == _r2_raw_expected, \ + "Payload mismatch: %r != %r" % (_r2_pkts[0].data, _r2_raw_expected) + += send(None) returns 0 immediately without error +_r3_sock = NativeJ1939Socket("vcan0", src_addr=0x32, promisc=False) +_r3_ret = _r3_sock.send(None) +_r3_sock.close() +assert _r3_ret == 0, "send(None) must return 0, got %r" % _r3_ret diff --git a/test/contrib/ldp.uts b/test/contrib/ldp.uts index af6bcbb17d5..409383de577 100644 --- a/test/contrib/ldp.uts +++ b/test/contrib/ldp.uts @@ -43,6 +43,16 @@ assert pkti.params == [180, 0, 0, 0, 0, '1.1.2.2', 0] = Build advanced LDPAddress() with LDPLabelMM() pkta = LDPAddress(address=['1.1.2.2', '172.16.2.1'])/LDPLabelMM(fec=[('172.16.2.0', 31)])/LDPLabelMM(fec=[('1.1.2.2', 32)])/LDPLabelMM(fec=[('1.1.2.1', 32)]) += LDP over UDP dissection +load_contrib("ldp") +pkt = IP()/UDP(sport=646, dport=646)/LDP()/LDPHello() +raw_pkt = raw(pkt) +dissected = IP(raw_pkt) +assert LDP in dissected +assert UDP in dissected +assert dissected[UDP].sport == 646 +assert LDPHello in dissected + = Advanced dissection - complex LDP load_contrib("mpls") pkt = Ether(b"\xcc\x04\x04\xdc\x00\x10\xcc\x03\x04\xdc\x00\x10\x88G\x00\x01-\xfeE\xc0\x014\xfe\x84\x00\x00\xff\x06\xb5z\x01\x01\x02\x02\x01\x01\x02\x01\xe4\xe4\x02\x86\xbf\xfb'\xe4\xb9\xb3\xe4GP\x10\x0e\xb6v\x9f\x00\x00\x00\x01\x01\x08\x01\x01\x02\x02\x00\x00\x03\x00\x00\x12\x00\x00\x00\x0e\x01\x01\x00\n\x00\x01\x01\x01\x02\x02\xac\x10\x02\x01\x04\x00\x00\x18\x00\x00\x00\x0f\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x02\x00\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x10\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x02\x02\x00\x00\x04\x00\x00\x00\x03\x04\x00\x00\x18\x00\x00\x00\x11\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x02\x01\x02\x00\x00\x04\x00\x00\x00\x12\x04\x00\x00\x18\x00\x00\x00\x12\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x02\x02\x00\x00\x04\x00\x00\x00\x13\x04\x00\x00\x18\x00\x00\x00\x13\x01\x00\x00\x08\x02\x00\x01 \x01\x01\x01\x01\x02\x00\x00\x04\x00\x00\x00\x14\x04\x00\x00\x18\x00\x00\x00\x14\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x01\x00\x02\x00\x00\x04\x00\x00\x00\x15\x04\x00\x00\x18\x00\x00\x00\x15\x01\x00\x00\x08\x02\x00\x01\x1f\xac\x10\x00\x00\x02\x00\x00\x04\x00\x00\x00\x16\x04\x00\x00$\x00\x00\x00\x16\x01\x00\x00\x14\x80\x80\x05\x0c\x00\x00\x00\x00\x00\x00\x00\n\x01\x04\x05\xdc\x0c\x04\x03\x02\x02\x00\x00\x04\x00\x00\x00\x10") diff --git a/test/regression.uts b/test/regression.uts index af7738a41e0..8f0592b486d 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -32,7 +32,7 @@ class FakeModule2(object): __version__ = "5.143.3.12" class FakeModule3(object): - __version__ = "v2.4.2.dev42" + __version__ = "v2.4.2.post42" from scapy.config import _version_checker @@ -67,7 +67,7 @@ with mock.patch('scapy.subprocess.Popen', return_value=b): except ValueError: pass -assert GitModuleScapy.__version__ == '2.4.5rc1.dev261' +assert GitModuleScapy.__version__ == '2.4.5rc1.post261' assert _version_checker(GitModuleScapy, (2, 4, 5)) = List layers @@ -616,6 +616,71 @@ c = pickle.loads(b) assert c[IP].dst == "192.168.0.1" assert raw(c) == raw(a) += Pickle preserves field values, payload and metadata + +import pickle + +p = IP(src='1.2.3.4', dst='5.6.7.8')/TCP(sport=1234, dport=80, flags='S') +p.time = 12345.0 +p.sent_time = 12346.0 +p.direction = 1 +p.sniffed_on = 'eth0' +p.wirelen = 100 +p.comment = b'test comment' +p2 = pickle.loads(pickle.dumps(p)) +assert p2[IP].src == '1.2.3.4' +assert p2[IP].dst == '5.6.7.8' +assert p2[TCP].sport == 1234 +assert p2[TCP].dport == 80 +assert p2[TCP].flags == 'S' +assert p2.time == 12345.0 +assert p2.sent_time == 12346.0 +assert p2.direction == 1 +assert p2.sniffed_on == 'eth0' +assert p2.wirelen == 100 +assert p2.comment == b'test comment' +assert raw(p2) == raw(p) + += Pickle a bare packet without payload + +import pickle + +p = IP(src='10.0.0.1') +p2 = pickle.loads(pickle.dumps(p)) +assert p2.src == '10.0.0.1' +assert raw(p2) == raw(p) + += Pickle preserves custom __slots__ from subclasses + +import pickle +import scapy.packet as _pkt_mod + +class _PickleTestPacket(Packet): + __slots__ = ["custom_id", "custom_tag"] + name = "PickleTestPacket" + fields_desc = [ByteField("val", 0)] + +# Make the class discoverable by pickle +_pkt_mod._PickleTestPacket = _PickleTestPacket +_PickleTestPacket.__module__ = 'scapy.packet' + +p = _PickleTestPacket(val=42) +p.custom_id = 0x123 +p.custom_tag = "hello" +p2 = pickle.loads(pickle.dumps(p)) +assert p2.val == 42 +assert p2.custom_id == 0x123 +assert p2.custom_tag == "hello" + +# Slots not explicitly set are not serialized +p3 = _PickleTestPacket(val=7) +assert not hasattr(p3, 'custom_id') +p4 = pickle.loads(pickle.dumps(p3)) +assert p4.val == 7 +assert not hasattr(p4, 'custom_id') + +del _pkt_mod._PickleTestPacket + = Usage test from scapy.main import _usage @@ -4777,6 +4842,45 @@ assert pl[0].wirelen == 1 assert pl[0][Ether].src == '00:11:22:33:44:55' assert pl[1][Ether].dst == '00:22:33:44:55:66' += __setstate__ accepts legacy tuple state (pickle backward compatibility) + +legacy = Ether(src='00:01:02:03:04:05', dst='00:06:07:08:09:0a')/Raw(b'legacy') +legacy.__setstate__(( + EDecimal("42.5"), + EDecimal("43.5"), + 1, + "legacy0", + 128, + b"legacy-comment", +)) +assert legacy.time == 42.5 +assert legacy.sent_time == 43.5 +assert legacy.direction == 1 +assert legacy.sniffed_on == "legacy0" +assert legacy.wirelen == 128 +assert legacy.comment == b"legacy-comment" +assert legacy.comments == [b"legacy-comment"] + += pickle roundtrip keeps packet metadata and comments list + +meta = Ether(src='10:11:12:13:14:15', dst='20:21:22:23:24:25')/Raw(b'meta') +meta.time = EDecimal("100.25") +meta.sent_time = EDecimal("100.75") +meta.direction = 2 +meta.sniffed_on = "meta0" +meta.wirelen = 256 +meta.comments = [b"first", b"second"] + +meta2 = pickle.loads(pickle.dumps(meta)) +assert bytes(meta2) == bytes(meta) +assert meta2.time == 100.25 +assert meta2.sent_time == 100.75 +assert meta2.direction == 2 +assert meta2.sniffed_on == "meta0" +assert meta2.wirelen == 256 +assert meta2.comments == [b"first", b"second"] +assert meta2.comment == b"first" + = EDecimal # GH4488 diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 9df33f0126b..0aafc3fcb0a 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -239,13 +239,55 @@ assert response[HCI_Cmd_Complete_Read_Local_Extended_Features].max_page == 2 assert response[HCI_Cmd_Complete_Read_Local_Extended_Features].extended_features == 0 assert response.answers(cmd) += LE Set Extended Scan Parameters + +cmd = HCI_Hdr(hex_bytes("0141200d00010500600060000020012001")) +assert HCI_Command_Hdr in cmd +assert cmd[HCI_Command_Hdr].ogf == 0x08 +assert cmd[HCI_Command_Hdr].ocf == 0x41 +assert cmd[HCI_Command_Hdr].len == 13 +assert HCI_Cmd_LE_Set_Extended_Scan_Parameters in cmd +scan = cmd[HCI_Cmd_LE_Set_Extended_Scan_Parameters] +assert scan.own_address_type == 0 +assert scan.scanning_filter_policy == 1 +assert scan.scanning_phys == 5 +assert scan.scan_type_1m == 0 +assert scan.scan_interval_1m == 96 +assert scan.scan_window_1m == 96 +assert scan.scan_type_coded == 0 +assert scan.scan_interval_coded == 288 +assert scan.scan_window_coded == 288 + += LE Extended Create Connection + +cmd = HCI_Hdr(hex_bytes("0143201a000001AABBCCDDEEFF01600060001800280000002a0000000000")) +assert HCI_Command_Hdr in cmd +assert cmd[HCI_Command_Hdr].ogf == 0x08 +assert cmd[HCI_Command_Hdr].ocf == 0x43 +assert cmd[HCI_Command_Hdr].len == 26 +assert HCI_Cmd_LE_Extended_Create_Connection in cmd +conn = cmd[HCI_Cmd_LE_Extended_Create_Connection] +assert conn.filter_policy == 0 +assert conn.address_type == 0 +assert conn.peer_addr_type == 1 +assert conn.peer_addr == "ff:ee:dd:cc:bb:aa" +assert conn.phys == 1 +assert conn.interval_1m == 96 +assert conn.window_1m == 96 +assert conn.min_interval_1m == 24 +assert conn.max_interval_1m == 40 +assert conn.latency_1m == 0 +assert conn.timeout_1m == 42 +assert conn.min_ce_1m == 0 +assert conn.max_ce_1m == 0 + = LE Create Connection # Request data cmd = HCI_Hdr(hex_bytes("010d2019600060000001123456677890001800280000002a0000000000")) assert HCI_Cmd_LE_Create_Connection in cmd -assert cmd[HCI_Cmd_LE_Create_Connection].paddr == '90:78:67:56:34:12' -assert cmd[HCI_Cmd_LE_Create_Connection].patype == 1 +assert cmd[HCI_Cmd_LE_Create_Connection].peer_addr == '90:78:67:56:34:12' +assert cmd[HCI_Cmd_LE_Create_Connection].peer_addr_type == 1 # Response data pending = HCI_Hdr(hex_bytes("040f0400020d20")) @@ -253,7 +295,7 @@ assert pending.answers(cmd) complete = HCI_Hdr(hex_bytes("043e1301020000000112345667789000000000000000")) assert HCI_LE_Meta_Connection_Complete in complete -assert complete[HCI_LE_Meta_Connection_Complete].paddr == '90:78:67:56:34:12' +assert complete[HCI_LE_Meta_Connection_Complete].peer_addr == '90:78:67:56:34:12' assert complete.answers(cmd) # Invalid combinations @@ -333,6 +375,29 @@ assert evt_pkt[HCI_Event_Connection_Complete].bd_addr == "54:b7:e5:91:34:09" assert evt_pkt[HCI_Event_Connection_Complete].link_type == 1 assert evt_pkt[HCI_Event_Connection_Complete].encryption_enabled == 0 += Connection Request +# Sample from a real BR/EDR controller HCI snoop log. Event 0x04 is sent by +# the controller when a remote BR/EDR/SCO device tries to connect. +# Layout: HCI_Hdr type=0x04, event code 0x04, len 0x0a, BD_ADDR (6 LE bytes), +# device_class (3 LE bytes), link_type (1 byte). +evt_raw_data = hex_bytes("04" "04" "0a" "8c5b6fd28330" "0c025a" "01") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Connection_Request in evt_pkt +assert evt_pkt[HCI_Event_Connection_Request].bd_addr == "30:83:d2:6f:5b:8c" +assert evt_pkt[HCI_Event_Connection_Request].device_class == 0x5a020c +assert evt_pkt[HCI_Event_Connection_Request].link_type == 1 # ACL connection +assert raw(evt_pkt) == evt_raw_data + +# Roundtrip build +built = HCI_Hdr() / HCI_Event_Hdr() / HCI_Event_Connection_Request( + bd_addr="aa:bb:cc:dd:ee:ff", device_class=0x240404, link_type=2) +parsed = HCI_Hdr(raw(built)) +assert parsed[HCI_Event_Hdr].code == 0x04 +assert parsed[HCI_Event_Hdr].len == 10 +assert parsed[HCI_Event_Connection_Request].bd_addr == "aa:bb:cc:dd:ee:ff" +assert parsed[HCI_Event_Connection_Request].device_class == 0x240404 +assert parsed[HCI_Event_Connection_Request].link_type == 2 + = Disconnection Complete evt_raw_data = hex_bytes("04050400400016") evt_pkt = HCI_Hdr(evt_raw_data) @@ -365,6 +430,18 @@ assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].status == 0 assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].handle == 0x000b assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].lmp_features == 0x875bffdbfe8ffeff += Remote Host Supported Features Notification +# Sample from a real BR/EDR controller HCI snoop log. Event code 0x3d is +# emitted after the controller learns the remote host's LMP features page. +evt_raw_data = hex_bytes("04" "3d" "0e" "f472de5c0530" "0f00000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Remote_Host_Supported_Features_Notification in evt_pkt +notif = evt_pkt[HCI_Event_Remote_Host_Supported_Features_Notification] +assert notif.bd_addr == "30:05:5c:de:72:f4" +assert notif.host_supported_features == 0x000000000000000f +# 0x0f -> first four LMP features set +assert raw(evt_pkt) == evt_raw_data + = Read Remote Version Information Complete evt_raw_data = hex_bytes("040c080002000bb0022c04") evt_pkt = HCI_Hdr(evt_raw_data) @@ -375,6 +452,18 @@ assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].version == 0x assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].manufacturer_name == 0x02b0 assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].subversion == 1068 += Command Complete LE Set Extended Scan Enable +evt_raw_data = hex_bytes("040e0402422000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Hdr in evt_pkt +assert evt_pkt[HCI_Event_Hdr].code == 0x0E +assert evt_pkt[HCI_Event_Hdr].len == 4 +assert HCI_Event_Command_Complete in evt_pkt +evt = evt_pkt[HCI_Event_Command_Complete] +assert evt.number == 2 +assert evt.opcode == 0x2042 +assert evt.status == 0 + = Command Complete evt_raw_data = hex_bytes("040e0a010b04002587ceedd668") evt_pkt = HCI_Hdr(evt_raw_data) @@ -447,6 +536,55 @@ evt_pkt = HCI_Hdr(evt_raw_data) assert HCI_Event_LE_Meta in evt_pkt assert evt_pkt[HCI_Event_LE_Meta].event == 0x14 += LE Meta Extended Advertising Report +evt_raw_data = hex_bytes("043e390d01100001AABBCCDDEEFF0100ff7fa60000000000000000001f1eff06000100000031") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Hdr in evt_pkt +assert evt_pkt[HCI_Event_Hdr].code == 0x3E +assert evt_pkt[HCI_Event_Hdr].len == 57 +assert HCI_LE_Meta_Extended_Advertising_Report in evt_pkt +assert evt_pkt.num_reports == 1 +report = evt_pkt[HCI_LE_Meta_Extended_Advertising_Report] +assert report.reserved0 == 0 +assert report.data_status == 0 +assert report.legacy == 1 +assert report.scan_response == 0 +assert report.directed == 0 +assert report.scannable == 0 +assert report.connectable == 0 +assert report.reserved == 0 +assert report.addr_type == 1 +assert report.addr == "ff:ee:dd:cc:bb:aa" +assert report.primary_phy == 1 +assert report.secondary_phy == 0 +assert report.advertising_sid == 255 +assert report.tx_power == 127 +assert report.rssi == -90 +assert report.periodic_advertising_interval == 0 +assert report.direct_addr_type == 0 +assert report.direct_addr == "00:00:00:00:00:00" +assert report.data_length == 31 + += LE Meta Enhanced Connection Complete +evt_raw_data = hex_bytes("043e1f0a0010000001AABBCCDDEEFF000000000000000000000000240000002a0000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Hdr in evt_pkt +assert evt_pkt[HCI_Event_Hdr].code == 0x3E +assert evt_pkt[HCI_Event_Hdr].len == 31 +assert HCI_LE_Meta_Enhanced_Connection_Complete in evt_pkt +evt = evt_pkt[HCI_LE_Meta_Enhanced_Connection_Complete] +assert evt.status == 0 +assert evt.handle == 16 +assert evt.role == 0 +assert evt.peer_addr_type == 1 +assert evt.peer_addr == "ff:ee:dd:cc:bb:aa" +assert evt.local_rpa == "00:00:00:00:00:00" +assert evt.peer_rpa == "00:00:00:00:00:00" +assert evt.interval == 36 +assert evt.latency == 0 +assert evt.supervision == 42 +assert evt.master_clock_accuracy == 0x0 + = LE Connection Update Event evt_raw_data = hex_bytes("043e0a03004800140001003c00") evt_pkt = HCI_Hdr(evt_raw_data) @@ -455,6 +593,35 @@ assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].interval == 20 assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].latency == 1 assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].timeout == 60 += LE Meta LE Read Remote Features Complete +# Sample from a real BLE HCI snoop log: subevent 0x04 (LE Read Remote +# Features [Page 0] Complete) returns the remote peer's LE feature mask. +evt_raw_data = hex_bytes("04" "3e" "0c" "04" "00" "4000" "1f00000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_LE_Meta_LE_Read_Remote_Features_Complete in evt_pkt +evt = evt_pkt[HCI_LE_Meta_LE_Read_Remote_Features_Complete] +assert evt.status == 0 +assert evt.handle == 0x0040 +assert evt.le_features == 0x000000000000001f +assert raw(evt_pkt) == evt_raw_data + += HCI Event Vendor (vendor-specific debug event) +# Sample from a real HCI snoop log. Event code 0xff is reserved by the +# Bluetooth Core spec for vendor-specific debugging events; the body is an +# opaque, vendor-defined byte string. +evt_raw_data = hex_bytes("04" "ff" "04" "26000101") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Vendor in evt_pkt +assert evt_pkt[HCI_Event_Vendor].data == b"\x26\x00\x01\x01" +assert raw(evt_pkt) == evt_raw_data + +# Roundtrip with arbitrary payload +built = HCI_Hdr() / HCI_Event_Hdr() / HCI_Event_Vendor(data=b"\xde\xad\xbe\xef") +parsed = HCI_Hdr(raw(built)) +assert parsed[HCI_Event_Hdr].code == 0xff +assert parsed[HCI_Event_Hdr].len == 4 +assert parsed[HCI_Event_Vendor].data == b"\xde\xad\xbe\xef" + + Bluetooth LE Advertising / Scan Response Data Parsing = Parse EIR_IncompleteList32BitServiceUUIDs @@ -497,6 +664,56 @@ assert EIR_LEBluetoothDeviceAddress in p assert p[EIR_LEBluetoothDeviceAddress].addr_type == 0x0 assert p[EIR_LEBluetoothDeviceAddress].bd_addr == '4c:ba:d7:19:35:d9' += Parse EIR_RandomTargetAddress +# Type 0x18 (Random Target Address) — six little-endian BD_ADDR bytes +p = EIR_Hdr(hex_bytes("0718aabbccddeeff")) +assert EIR_RandomTargetAddress in p +assert p[EIR_RandomTargetAddress].bd_addr == "ff:ee:dd:cc:bb:aa" +assert raw(p) == hex_bytes("0718aabbccddeeff") +# Roundtrip build +built = EIR_Hdr() / EIR_RandomTargetAddress(bd_addr="11:22:33:44:55:66") +assert raw(built) == hex_bytes("0718665544332211") + += Parse EIR_LERole +# Type 0x1c (LE Role) — single byte selecting one of four GAP roles. +for role in (0, 1, 2, 3): + p = EIR_Hdr(hex_bytes("021c%02x" % role)) + assert EIR_LERole in p + assert p[EIR_LERole].role == role + assert raw(p) == hex_bytes("021c%02x" % role) + += Parse EIR_BroadcastName +# Type 0x30 (Broadcast Name) — sample observed in a real scan response from +# a Sony BRAVIA TV advertising for LE Audio broadcast assistant clients. +raw_data = hex_bytes("0a30" "416e746520526f6f6d") # "Ante Room" +p = EIR_Hdr(raw_data) +assert EIR_BroadcastName in p +assert p[EIR_BroadcastName].broadcast_name == b"Ante Room" +assert raw(p) == raw_data +# Roundtrip build +built = EIR_Hdr() / EIR_BroadcastName(broadcast_name=b"Speaker 1") +assert raw(built) == hex_bytes("0a30") + b"Speaker 1" + += Parse EIR_3DInformation +# Type 0x3d (3D Information) — sample from a real BR/EDR Extended Inquiry +# Response (Sony XBR-65X850F display, observed in an HCI snoop log). +raw_data = hex_bytes("033d0764") +p = EIR_Hdr(raw_data) +assert EIR_3DInformation in p +info = p[EIR_3DInformation] +assert info.factory_test_mode == 0 +assert info.send_battery_level_on_startup == 1 +assert info.battery_level_reporting == 1 +assert info.association_notification == 1 +assert info.path_loss_threshold == 100 +assert raw(p) == raw_data +# Roundtrip build with factory_test_mode bit set +built = EIR_Hdr() / EIR_3DInformation(factory_test_mode=1, path_loss_threshold=0x80) +assert raw(built) == hex_bytes("033d8080") +parsed = EIR_Hdr(raw(built)) +assert parsed[EIR_3DInformation].factory_test_mode == 1 +assert parsed[EIR_3DInformation].path_loss_threshold == 0x80 + = Parse EIR_Appearance p = BTLE(hex_bytes("d6be898e201660d4d3cebffb0201050319420c0303e7fe040948393850c27c")) assert EIR_Appearance in p @@ -608,8 +825,8 @@ assert raw(p[EIR_ServiceData16BitUUID].payload) == hex_bytes("e6c2") = Basic L2CAP dissect a = L2CAP_Hdr(b'\x08\x00\x06\x00\t\x00\xf6\xe5\xd4\xc3\xb2\xa1') -assert a[SM_Identity_Address_Information].address == 'a1:b2:c3:d4:e5:f6' -assert a[SM_Identity_Address_Information].atype == 0 +assert a[SM_Identity_Address_Information].addr == 'a1:b2:c3:d4:e5:f6' +assert a[SM_Identity_Address_Information].addr_type == 0 a.show() = Basic HCI_ACL_Hdr build & dissect @@ -723,8 +940,8 @@ a = HCI_Hdr()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_LE_Meta_Extended_Advertisi #event_type = 0x0012, scannable = 1, legacy = 1, - address_type = 0x01, - address="a1:b2:c3:d4:e5:f6", + addr_type = 0x01, + addr="a1:b2:c3:d4:e5:f6", primary_phy = 1, rssi = -85, data=[ @@ -741,8 +958,8 @@ a = HCI_Hdr()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_LE_Meta_Extended_Advertisi scannable = 1, scan_response = 1, legacy = 1, - address_type = 0x01, - address="a1:b2:c3:d4:e5:f6", + addr_type = 0x01, + addr="a1:b2:c3:d4:e5:f6", primary_phy = 1, rssi = -85, data=[ @@ -759,7 +976,7 @@ b = HCI_Hdr(raw(a)) b.show() assert b[HCI_Event_Hdr].len > 0 assert b[HCI_LE_Meta_Extended_Advertising_Reports].num_reports == 2 -assert b[HCI_LE_Meta_Extended_Advertising_Report][0].address == "a1:b2:c3:d4:e5:f6" +assert b[HCI_LE_Meta_Extended_Advertising_Report][0].addr == "a1:b2:c3:d4:e5:f6" assert b[HCI_LE_Meta_Extended_Advertising_Report][0].tx_power == 0x7f assert b[HCI_LE_Meta_Extended_Advertising_Report][0].rssi == -85 assert b[HCI_LE_Meta_Extended_Advertising_Report][0].data_length > 0 @@ -821,6 +1038,22 @@ assert r == b'\rscapy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' p = SM_Hdr(r) assert SM_DHKey_Check in p and p.dhkey_check[:5] == b"scapy" += SM_Keypress_Notification() tests +# SMP opcode 0x0e (Keypress Notification) is used during passkey entry to +# echo each keypress event from one peer to the other. The body is a single +# byte selecting which keypress phase the notification represents. +for nt in range(5): + r = raw(SM_Hdr() / SM_Keypress_Notification(notification_type=nt)) + assert r == bytes([0x0e, nt]) + p = SM_Hdr(r) + assert SM_Keypress_Notification in p + assert p[SM_Keypress_Notification].notification_type == nt + +# Wrapping in L2CAP on the SMP fixed channel (cid=0x0006) parses too. +pkt = HCI_Hdr(hex_bytes('0200260600020006000e02')) +assert SM_Keypress_Notification in pkt +assert pkt[SM_Keypress_Notification].notification_type == 0x02 + + HCIMon tests diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f56d116a505..f65c75d89ce 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -992,3 +992,2960 @@ for _ in range(50): pass roundtrip_count >= 45 + +########### CBOR Fields ########################################### + ++ CBORF scalar fields - CBORF_UNSIGNED_INTEGER + += CBORF_UNSIGNED_INTEGER basic encode/decode +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktUInt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 42) + +pkt = PktUInt() +assert pkt.value.val == 42 +raw_data = bytes(pkt) +pkt2 = PktUInt(raw_data) +assert pkt2.value.val == 42 + += CBORF_UNSIGNED_INTEGER zero value +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktUIntZero(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + +pkt = PktUIntZero() +raw_data = bytes(pkt) +assert raw_data == b'\x00' +pkt2 = PktUIntZero(raw_data) +assert pkt2.value.val == 0 + += CBORF_UNSIGNED_INTEGER large value roundtrip +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktUIntLarge(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 1000000) + +pkt = PktUIntLarge() +raw_data = bytes(pkt) +pkt2 = PktUIntLarge(raw_data) +assert pkt2.value.val == 1000000 + ++ CBORF scalar fields - CBORF_NEGATIVE_INTEGER + += CBORF_NEGATIVE_INTEGER basic encode/decode +from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktNInt(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER("value", -1) + +pkt = PktNInt() +assert pkt.value.val == -1 +raw_data = bytes(pkt) +pkt2 = PktNInt(raw_data) +assert pkt2.value.val == -1 + += CBORF_NEGATIVE_INTEGER -100 roundtrip +from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktNInt100(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER("value", -100) + +pkt = PktNInt100() +raw_data = bytes(pkt) +pkt2 = PktNInt100(raw_data) +assert pkt2.value.val == -100 + ++ CBORF scalar fields - CBORF_INTEGER + += CBORF_INTEGER positive value +from scapy.cbor.cborfields import CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktInt(CBOR_Packet): + CBOR_root = CBORF_INTEGER("value", 7) + +pkt = PktInt() +raw_data = bytes(pkt) +pkt2 = PktInt(raw_data) +assert pkt2.value.val == 7 + += CBORF_INTEGER negative value +from scapy.cbor.cborfields import CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PktIntNeg(CBOR_Packet): + CBOR_root = CBORF_INTEGER("value", -5) + +pkt = PktIntNeg() +raw_data = bytes(pkt) +pkt2 = PktIntNeg(raw_data) +assert pkt2.value.val == -5 + ++ CBORF scalar fields - CBORF_BYTE_STRING + += CBORF_BYTE_STRING basic encode/decode +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class PktBStr(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("data", b"hello") + +pkt = PktBStr() +assert pkt.data.val == b"hello" +raw_data = bytes(pkt) +pkt2 = PktBStr(raw_data) +assert pkt2.data.val == b"hello" + += CBORF_BYTE_STRING empty bytes +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class PktBStrEmpty(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("data", b"") + +pkt = PktBStrEmpty() +raw_data = bytes(pkt) +assert raw_data == b'\x40' +pkt2 = PktBStrEmpty(raw_data) +assert pkt2.data.val == b"" + ++ CBORF scalar fields - CBORF_TEXT_STRING + += CBORF_TEXT_STRING basic encode/decode +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class PktTStr(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING("title", "hello") + +pkt = PktTStr() +assert pkt.title.val == "hello" +raw_data = bytes(pkt) +pkt2 = PktTStr(raw_data) +assert pkt2.title.val == "hello" + += CBORF_TEXT_STRING empty string +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class PktTStrEmpty(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING("title", "") + +pkt = PktTStrEmpty() +raw_data = bytes(pkt) +assert raw_data == b'\x60' +pkt2 = PktTStrEmpty(raw_data) +assert pkt2.title.val == "" + ++ CBORF scalar fields - CBORF_BOOLEAN + += CBORF_BOOLEAN true value +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cbor.cbor import CBOR_TRUE +from scapy.cborpacket import CBOR_Packet + +class PktBool(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN("flag", True) + +pkt = PktBool() +assert isinstance(pkt.flag, CBOR_TRUE) +raw_data = bytes(pkt) +assert raw_data == b'\xf5' +pkt2 = PktBool(raw_data) +assert isinstance(pkt2.flag, CBOR_TRUE) + += CBORF_BOOLEAN false value +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cbor.cbor import CBOR_FALSE +from scapy.cborpacket import CBOR_Packet + +class PktBoolFalse(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN("flag", False) + +pkt = PktBoolFalse() +raw_data = bytes(pkt) +assert raw_data == b'\xf4' +pkt2 = PktBoolFalse(raw_data) +assert isinstance(pkt2.flag, CBOR_FALSE) + ++ CBORF scalar fields - CBORF_FLOAT + += CBORF_FLOAT encode/decode +from scapy.cbor.cborfields import CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class PktFloat(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 1.5) + +pkt = PktFloat() +raw_data = bytes(pkt) +pkt2 = PktFloat(raw_data) +assert abs(pkt2.value.val - 1.5) < 1e-9 + += CBORF_NULL encode/decode +from scapy.cbor.cborfields import CBORF_NULL +from scapy.cbor.cbor import CBOR_NULL +from scapy.cborpacket import CBOR_Packet + +class PktNull(CBOR_Packet): + CBOR_root = CBORF_NULL("nothing") + +pkt = PktNull() +raw_data = bytes(pkt) +assert raw_data == b'\xf6' +pkt2 = PktNull(raw_data) +assert isinstance(pkt2.nothing, CBOR_NULL) + ++ CBORF scalar fields - CBORF_UNDEFINED + += CBORF_UNDEFINED encode/decode +from scapy.cbor.cborfields import CBORF_UNDEFINED +from scapy.cbor.cbor import CBOR_UNDEFINED +from scapy.cborpacket import CBOR_Packet + +class PktUndef(CBOR_Packet): + CBOR_root = CBORF_UNDEFINED("undef") + +pkt = PktUndef() +raw_data = bytes(pkt) +assert raw_data == b'\xf7' +pkt2 = PktUndef(raw_data) +assert isinstance(pkt2.undef, CBOR_UNDEFINED) + ++ CBORF structured fields - CBORF_ARRAY + += CBORF_ARRAY two-field encode/decode +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("title", "test"), + ) + +pkt = MyCBOR() +assert pkt.version.val == 1 +assert pkt.title.val == "test" +raw_data = bytes(pkt) +pkt2 = MyCBOR(raw_data) +assert pkt2.version.val == 1 +assert pkt2.title.val == "test" + += CBORF_ARRAY three-field encode/decode +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class Multi(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("id", 99), + CBORF_TEXT_STRING("label", "x"), + CBORF_BOOLEAN("active", True), + ) + +pkt = Multi() +raw_data = bytes(pkt) +pkt2 = Multi(raw_data) +assert pkt2.id.val == 99 +assert pkt2.label.val == "x" + += CBORF_ARRAY single integer roundtrip +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class Single(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("count", 5), + ) + +pkt = Single() +raw_data = bytes(pkt) +pkt2 = Single(raw_data) +assert pkt2.count.val == 5 + ++ CBORF structured fields - CBORF_ARRAY_OF + += CBORF_ARRAY_OF with CBORF_INTEGER elements +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class ArrOfInt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("items", [], CBORF_INTEGER) + +pkt = ArrOfInt() +pkt.items = [CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(2), CBOR_UNSIGNED_INTEGER(3)] +raw_data = bytes(pkt) +pkt2 = ArrOfInt(raw_data) +assert len(pkt2.items) == 3 +assert pkt2.items[0].val == 1 +assert pkt2.items[2].val == 3 + ++ CBORF structured fields - CBORF_MAP + += CBORF_MAP basic encode/decode +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class MyMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER("version", 2), + CBORF_TEXT_STRING("title", "cbor"), + ) + +pkt = MyMap() +assert pkt.version.val == 2 +assert pkt.title.val == "cbor" +raw_data = bytes(pkt) +pkt2 = MyMap(raw_data) +assert pkt2.version.val == 2 +assert pkt2.title.val == "cbor" + += CBORF_MAP byte string value +from scapy.cbor.cborfields import CBORF_MAP, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class BinMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_BYTE_STRING("data", b"\xde\xad\xbe\xef"), + ) + +pkt = BinMap() +raw_data = bytes(pkt) +pkt2 = BinMap(raw_data) +assert pkt2.data.val == b"\xde\xad\xbe\xef" + ++ CBORF complex fields - CBORF_optional + += CBORF_optional present field +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class OptPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("version", 1), + CBORF_optional(CBORF_TEXT_STRING("title", "")), + ) + +pkt = OptPkt() +raw_data = bytes(pkt) +pkt2 = OptPkt(raw_data) +assert pkt2.version.val == 1 +assert pkt2.title.val == "" + ++ CBORF_PACKET nested packet + += CBORF_PACKET basic nesting +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class Inner(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("x", 10), + ) + +class Outer(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_TEXT_STRING("label", "outer"), + CBORF_PACKET("inner", None, Inner), + ) + +inner = Inner() +outer = Outer() +outer.label = outer.label # keep default +outer.inner = inner +raw_data = bytes(outer) +outer2 = Outer(raw_data) +assert outer2.label.val == "outer" + ++ CBORF_SEMANTIC_TAG + += CBORF_SEMANTIC_TAG encode with inner integer +from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_INTEGER +from scapy.cbor.cbor import CBOR_SEMANTIC_TAG as CBOR_SEM +from scapy.cborpacket import CBOR_Packet + +class TaggedPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG("tag_info", None, 1, CBORF_INTEGER("ts", 0)) + +pkt = TaggedPkt() +# Build encodes tag 1 + inner field default +raw_data = bytes(pkt) +# Major type 6 (tag), tag number 1 => 0xc1 +assert raw_data[0:1] == b'\xc1' + ++ CBOR_Packet / CBORF field integration + += CBOR_Packet fields_desc built from CBORF_ARRAY +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class Demo(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("id", 1), + CBORF_TEXT_STRING("desc", "demo"), + ) + +# fields_desc should contain both fields +field_names = [f.name for f in Demo.fields_desc] +assert "id" in field_names +assert "desc" in field_names + += CBOR_Packet roundtrip preserves raw bytes +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class Simple(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("a", 3), + CBORF_INTEGER("b", 7), + ) + +pkt = Simple() +raw_data = bytes(pkt) +pkt2 = Simple(raw_data) +assert bytes(pkt2) == raw_data + +########### Additional Unit Tests #################################### + ++ CBOR Simple Values + += Decode CBOR simple value 0 +data = bytes.fromhex('e0') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +from scapy.cbor.cbor import CBOR_SIMPLE_VALUE +isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 0 and remainder == b'' + += Decode CBOR simple value 16 +data = bytes.fromhex('f0') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 16 and remainder == b'' + += Decode CBOR simple value 255 (1-byte extended) +data = bytes.fromhex('f8ff') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 255 and remainder == b'' + ++ CBOR Float Encodings - RFC 8949 Test Vectors + += Half-precision: positive zero (0xf90000) +import math +data = bytes.fromhex('f90000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 0.0 and remainder == b'' + += Half-precision: negative zero (0xf98000) +data = bytes.fromhex('f98000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == -0.0 and math.copysign(1, obj.val) == -1.0 and remainder == b'' + += Half-precision: 1.0 (0xf93c00) +data = bytes.fromhex('f93c00') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 1.0 and remainder == b'' + += Half-precision: 1.5 (0xf93e00) +data = bytes.fromhex('f93e00') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 1.5 and remainder == b'' + += Half-precision: max (65504.0) (0xf97bff) +data = bytes.fromhex('f97bff') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 65504.0 and remainder == b'' + += Half-precision: smallest subnormal (0xf90001) +data = bytes.fromhex('f90001') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and abs(obj.val - 5.960464477539063e-8) < 1e-15 and remainder == b'' + += Half-precision: smallest normal (0xf90400) +data = bytes.fromhex('f90400') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and abs(obj.val - 6.103515625e-5) < 1e-12 and remainder == b'' + += Half-precision: positive infinity (0xf97c00) +data = bytes.fromhex('f97c00') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 and remainder == b'' + += Half-precision: negative infinity (0xf9fc00) +data = bytes.fromhex('f9fc00') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val < 0 and remainder == b'' + += Half-precision: NaN (0xf97e00) +data = bytes.fromhex('f97e00') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' + += Single-precision: 100000.0 (0xfa47c35000) +data = bytes.fromhex('fa47c35000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 100000.0 and remainder == b'' + += Single-precision: max float32 (0xfa7f7fffff) +data = bytes.fromhex('fa7f7fffff') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and abs(obj.val - 3.4028234663852886e+38) < 1e30 and remainder == b'' + += Single-precision: positive infinity (0xfa7f800000) +data = bytes.fromhex('fa7f800000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 and remainder == b'' + += Single-precision: NaN (0xfa7fc00000) +data = bytes.fromhex('fa7fc00000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' + += Double-precision: 1.1 (0xfb3ff199999999999a) +data = bytes.fromhex('fb3ff199999999999a') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and abs(obj.val - 1.1) < 1e-10 and remainder == b'' + += Double-precision: 1.0e+300 (0xfb7e37e43c8800759c) +data = bytes.fromhex('fb7e37e43c8800759c') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and abs(obj.val - 1.0e+300) / 1.0e+300 < 1e-10 and remainder == b'' + += Double-precision: NaN (0xfb7ff8000000000000) +data = bytes.fromhex('fb7ff8000000000000') +obj, remainder = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' + ++ CBOR Integer Encoding - RFC 8949 Test Vectors + += RFC 8949: encode 0 +obj = CBOR_UNSIGNED_INTEGER(0) +bytes(obj) == bytes.fromhex('00') + += RFC 8949: encode 1 +obj = CBOR_UNSIGNED_INTEGER(1) +bytes(obj) == bytes.fromhex('01') + += RFC 8949: encode 10 +obj = CBOR_UNSIGNED_INTEGER(10) +bytes(obj) == bytes.fromhex('0a') + += RFC 8949: encode 23 +obj = CBOR_UNSIGNED_INTEGER(23) +bytes(obj) == bytes.fromhex('17') + += RFC 8949: encode 24 +obj = CBOR_UNSIGNED_INTEGER(24) +bytes(obj) == bytes.fromhex('1818') + += RFC 8949: encode 25 +obj = CBOR_UNSIGNED_INTEGER(25) +bytes(obj) == bytes.fromhex('1819') + += RFC 8949: encode 100 +obj = CBOR_UNSIGNED_INTEGER(100) +bytes(obj) == bytes.fromhex('1864') + += RFC 8949: encode 1000 +obj = CBOR_UNSIGNED_INTEGER(1000) +bytes(obj) == bytes.fromhex('1903e8') + += RFC 8949: encode 1000000 +obj = CBOR_UNSIGNED_INTEGER(1000000) +bytes(obj) == bytes.fromhex('1a000f4240') + += RFC 8949: encode 1000000000000 +obj = CBOR_UNSIGNED_INTEGER(1000000000000) +bytes(obj) == bytes.fromhex('1b000000e8d4a51000') + += RFC 8949: encode 18446744073709551615 (2^64-1) +obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) +bytes(obj) == bytes.fromhex('1bffffffffffffffff') + += RFC 8949: encode -1 +obj = CBOR_NEGATIVE_INTEGER(-1) +bytes(obj) == bytes.fromhex('20') + += RFC 8949: encode -10 +obj = CBOR_NEGATIVE_INTEGER(-10) +bytes(obj) == bytes.fromhex('29') + += RFC 8949: encode -100 +obj = CBOR_NEGATIVE_INTEGER(-100) +bytes(obj) == bytes.fromhex('3863') + += RFC 8949: encode -1000 +obj = CBOR_NEGATIVE_INTEGER(-1000) +bytes(obj) == bytes.fromhex('3903e7') + += RFC 8949: decode 0 +obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('00')) +obj.val == 0 and remainder == b'' + += RFC 8949: decode 23 +obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('17')) +obj.val == 23 and remainder == b'' + += RFC 8949: decode 24 +obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1818')) +obj.val == 24 and remainder == b'' + += RFC 8949: decode 1000000000000 +obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1b000000e8d4a51000')) +obj.val == 1000000000000 and remainder == b'' + += RFC 8949: decode -1000 +obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('3903e7')) +obj.val == -1000 and remainder == b'' + ++ CBOR Byte String with All Byte Values + += CBOR_BYTE_STRING: encode/decode all 256 byte values +all_bytes = bytes(range(256)) +obj = CBOR_BYTE_STRING(all_bytes) +enc = bytes(obj) +dec, remainder = CBOR_Codecs.CBOR.dec(enc) +dec.val == all_bytes and remainder == b'' + += CBOR_BYTE_STRING: cbor2 interop with all 256 byte values +import cbor2 +all_bytes = bytes(range(256)) +obj = CBOR_BYTE_STRING(all_bytes) +enc = bytes(obj) +dec = cbor2.loads(enc) +dec == all_bytes + ++ CBOR Map with Integer Keys + += Decode map with integer keys (cbor2 encode, Scapy decode) +import cbor2 +enc = cbor2.dumps({1: 'one', 2: 'two', -1: 'minus_one'}) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_MAP) and obj.val.get(1) is not None and obj.val[1].val == 'one' and remainder == b'' + += Encode map with integer keys (Scapy encode, cbor2 decode) +from scapy.cbor.cborcodec import CBORcodec_MAP +enc = CBORcodec_MAP.enc({1: 'one', 2: 'two'}) +dec = cbor2.loads(enc) +dec == {1: 'one', 2: 'two'} + += Map with mixed key types roundtrip +enc = cbor2.dumps({'str_key': 42, 1: 'int_key'}) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_MAP) and len(obj.val) == 2 and remainder == b'' + ++ CBOR Multiple Items in Stream + += Decode three integers from a single byte stream +data = bytes.fromhex('01') + bytes.fromhex('0a') + bytes.fromhex('17') +obj1, rest1 = CBOR_Codecs.CBOR.dec(data) +obj2, rest2 = CBOR_Codecs.CBOR.dec(rest1) +obj3, rest3 = CBOR_Codecs.CBOR.dec(rest2) +obj1.val == 1 and obj2.val == 10 and obj3.val == 23 and rest3 == b'' + += Decode integer followed by string +data = bytes.fromhex('1864') + bytes.fromhex('626869') +obj1, rest1 = CBOR_Codecs.CBOR.dec(data) +obj2, rest2 = CBOR_Codecs.CBOR.dec(rest1) +obj1.val == 100 and obj2.val == 'hi' and rest2 == b'' + ++ CBOR Nested Structures Unit Tests + += Encode and decode doubly nested array +from scapy.cbor.cborcodec import CBORcodec_ARRAY +enc = CBORcodec_ARRAY.enc([[1, 2], [3, 4], [5, 6]]) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and len(obj.val[0].val) == 2 and remainder == b'' + += Encode and decode map containing arrays +from scapy.cbor.cborcodec import CBORcodec_MAP, CBORcodec_ARRAY +enc = CBORcodec_MAP.enc({'nums': [1, 2, 3], 'strs': ['a', 'b']}) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_MAP) and 'nums' in obj.val and isinstance(obj.val['nums'], CBOR_ARRAY) and remainder == b'' + += Encode and decode array containing maps +from scapy.cbor.cborcodec import CBORcodec_ARRAY +enc = CBORcodec_ARRAY.enc([{'id': 1}, {'id': 2}]) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and isinstance(obj.val[0], CBOR_MAP) and remainder == b'' + +########### Extended Interoperability Tests with cbor2 ################ + ++ CBOR Interoperability - RFC 8949 Appendix B (Scapy encode, cbor2 decode) + += RFC 8949 Appendix B: 0 +import cbor2 +obj = CBOR_UNSIGNED_INTEGER(0) +cbor2.loads(bytes(obj)) == 0 + += RFC 8949 Appendix B: 1 +obj = CBOR_UNSIGNED_INTEGER(1) +cbor2.loads(bytes(obj)) == 1 + += RFC 8949 Appendix B: 10 +obj = CBOR_UNSIGNED_INTEGER(10) +cbor2.loads(bytes(obj)) == 10 + += RFC 8949 Appendix B: 23 +obj = CBOR_UNSIGNED_INTEGER(23) +cbor2.loads(bytes(obj)) == 23 + += RFC 8949 Appendix B: 24 +obj = CBOR_UNSIGNED_INTEGER(24) +cbor2.loads(bytes(obj)) == 24 + += RFC 8949 Appendix B: 1000 +obj = CBOR_UNSIGNED_INTEGER(1000) +cbor2.loads(bytes(obj)) == 1000 + += RFC 8949 Appendix B: 1000000000000 +obj = CBOR_UNSIGNED_INTEGER(1000000000000) +cbor2.loads(bytes(obj)) == 1000000000000 + += RFC 8949 Appendix B: 18446744073709551615 (max u64) +obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) +cbor2.loads(bytes(obj)) == 18446744073709551615 + += RFC 8949 Appendix B: -1 +obj = CBOR_NEGATIVE_INTEGER(-1) +cbor2.loads(bytes(obj)) == -1 + += RFC 8949 Appendix B: -1000 +obj = CBOR_NEGATIVE_INTEGER(-1000) +cbor2.loads(bytes(obj)) == -1000 + += RFC 8949 Appendix B: false +obj = CBOR_FALSE() +cbor2.loads(bytes(obj)) is False + += RFC 8949 Appendix B: true +obj = CBOR_TRUE() +cbor2.loads(bytes(obj)) is True + += RFC 8949 Appendix B: null +obj = CBOR_NULL() +cbor2.loads(bytes(obj)) is None + += RFC 8949 Appendix B: undefined +obj = CBOR_UNDEFINED() +decoded = cbor2.loads(bytes(obj)) +from cbor2 import undefined +decoded is undefined + += RFC 8949 Appendix B: empty byte string +obj = CBOR_BYTE_STRING(b'') +cbor2.loads(bytes(obj)) == b'' + += RFC 8949 Appendix B: byte string b'\x01\x02\x03\x04' +obj = CBOR_BYTE_STRING(b'\x01\x02\x03\x04') +cbor2.loads(bytes(obj)) == b'\x01\x02\x03\x04' + += RFC 8949 Appendix B: empty text string +obj = CBOR_TEXT_STRING('') +cbor2.loads(bytes(obj)) == '' + += RFC 8949 Appendix B: 'a' +obj = CBOR_TEXT_STRING('a') +cbor2.loads(bytes(obj)) == 'a' + += RFC 8949 Appendix B: 'IETF' +obj = CBOR_TEXT_STRING('IETF') +cbor2.loads(bytes(obj)) == 'IETF' + += RFC 8949 Appendix B: u00fc (ü) +obj = CBOR_TEXT_STRING('\u00fc') +cbor2.loads(bytes(obj)) == '\u00fc' + += RFC 8949 Appendix B: u6c34 (water in Chinese) +obj = CBOR_TEXT_STRING('\u6c34') +cbor2.loads(bytes(obj)) == '\u6c34' + += RFC 8949 Appendix B: empty array +from scapy.cbor.cborcodec import CBORcodec_ARRAY +enc = CBORcodec_ARRAY.enc([]) +cbor2.loads(enc) == [] + += RFC 8949 Appendix B: [1, 2, 3] +enc = CBORcodec_ARRAY.enc([1, 2, 3]) +cbor2.loads(enc) == [1, 2, 3] + += RFC 8949 Appendix B: [1, [2, 3], [4, 5]] +enc = CBORcodec_ARRAY.enc([1, [2, 3], [4, 5]]) +cbor2.loads(enc) == [1, [2, 3], [4, 5]] + += RFC 8949 Appendix B: empty map +from scapy.cbor.cborcodec import CBORcodec_MAP +enc = CBORcodec_MAP.enc({}) +cbor2.loads(enc) == {} + += RFC 8949 Appendix B: {1: 2, 3: 4} +enc = CBORcodec_MAP.enc({1: 2, 3: 4}) +cbor2.loads(enc) == {1: 2, 3: 4} + += RFC 8949 Appendix B: {"a": 1, "b": [2, 3]} +enc = CBORcodec_MAP.enc({"a": 1, "b": [2, 3]}) +cbor2.loads(enc) == {"a": 1, "b": [2, 3]} + ++ CBOR Interoperability - RFC 8949 Appendix B (cbor2 encode, Scapy decode) + += RFC 8949 Appendix B decode: 0 +import cbor2 +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(0)) +obj.val == 0 and isinstance(obj, CBOR_UNSIGNED_INTEGER) + += RFC 8949 Appendix B decode: 23 +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(23)) +obj.val == 23 and isinstance(obj, CBOR_UNSIGNED_INTEGER) + += RFC 8949 Appendix B decode: 24 +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(24)) +obj.val == 24 and isinstance(obj, CBOR_UNSIGNED_INTEGER) + += RFC 8949 Appendix B decode: -1 +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1)) +obj.val == -1 and isinstance(obj, CBOR_NEGATIVE_INTEGER) + += RFC 8949 Appendix B decode: -1000 +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1000)) +obj.val == -1000 and isinstance(obj, CBOR_NEGATIVE_INTEGER) + += RFC 8949 Appendix B decode: false +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(False)) +isinstance(obj, CBOR_FALSE) and obj.val is False + += RFC 8949 Appendix B decode: true +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(True)) +isinstance(obj, CBOR_TRUE) and obj.val is True + += RFC 8949 Appendix B decode: null +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(None)) +isinstance(obj, CBOR_NULL) and obj.val is None + += RFC 8949 Appendix B decode: empty string +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('')) +isinstance(obj, CBOR_TEXT_STRING) and obj.val == '' + += RFC 8949 Appendix B decode: 'IETF' +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('IETF')) +isinstance(obj, CBOR_TEXT_STRING) and obj.val == 'IETF' + += RFC 8949 Appendix B decode: u00fc +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('\u00fc')) +isinstance(obj, CBOR_TEXT_STRING) and obj.val == '\u00fc' + += RFC 8949 Appendix B decode: b'\x01\x02\x03\x04' +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(b'\x01\x02\x03\x04')) +isinstance(obj, CBOR_BYTE_STRING) and obj.val == b'\x01\x02\x03\x04' + += RFC 8949 Appendix B decode: [1, 2, 3] +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps([1, 2, 3])) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and obj.val[0].val == 1 + += RFC 8949 Appendix B decode: [1, [2, 3], [4, 5]] +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps([1, [2, 3], [4, 5]])) +isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and isinstance(obj.val[1], CBOR_ARRAY) + += RFC 8949 Appendix B decode: {"a": 1, "b": [2, 3]} +obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps({"a": 1, "b": [2, 3]})) +isinstance(obj, CBOR_MAP) and obj.val['a'].val == 1 and isinstance(obj.val['b'], CBOR_ARRAY) + ++ CBOR Interoperability - Byte-exact Comparison + += Scapy and cbor2 produce identical bytes for integer 0 +import cbor2 +bytes(CBOR_UNSIGNED_INTEGER(0)) == cbor2.dumps(0) + += Scapy and cbor2 produce identical bytes for integer 255 +bytes(CBOR_UNSIGNED_INTEGER(255)) == cbor2.dumps(255) + += Scapy and cbor2 produce identical bytes for -1 +bytes(CBOR_NEGATIVE_INTEGER(-1)) == cbor2.dumps(-1) + += Scapy and cbor2 produce identical bytes for -1000 +bytes(CBOR_NEGATIVE_INTEGER(-1000)) == cbor2.dumps(-1000) + += Scapy and cbor2 produce identical bytes for empty byte string +bytes(CBOR_BYTE_STRING(b'')) == cbor2.dumps(b'') + += Scapy and cbor2 produce identical bytes for 'hello' +bytes(CBOR_TEXT_STRING('hello')) == cbor2.dumps('hello') + += Scapy and cbor2 produce identical bytes for true +bytes(CBOR_TRUE()) == cbor2.dumps(True) + += Scapy and cbor2 produce identical bytes for false +bytes(CBOR_FALSE()) == cbor2.dumps(False) + += Scapy and cbor2 produce identical bytes for null +bytes(CBOR_NULL()) == cbor2.dumps(None) + += Scapy and cbor2 produce identical bytes for undefined +from cbor2 import undefined +bytes(CBOR_UNDEFINED()) == cbor2.dumps(undefined) + += Scapy and cbor2 produce identical bytes for empty array +from scapy.cbor.cborcodec import CBORcodec_ARRAY +CBORcodec_ARRAY.enc([]) == cbor2.dumps([]) + += Scapy and cbor2 produce identical bytes for empty map +from scapy.cbor.cborcodec import CBORcodec_MAP +CBORcodec_MAP.enc({}) == cbor2.dumps({}) + += Scapy and cbor2 produce identical bytes for [1, 2, 3] +CBORcodec_ARRAY.enc([1, 2, 3]) == cbor2.dumps([1, 2, 3]) + += Scapy and cbor2 produce identical bytes for {'a': 1} +CBORcodec_MAP.enc({'a': 1}) == cbor2.dumps({'a': 1}) + ++ CBOR Interoperability - Semantic Tags + += Scapy encode semantic tag (tag 42), cbor2 decode +import cbor2 +obj = CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('test-content'))) +enc = bytes(obj) +dec = cbor2.loads(enc) +isinstance(dec, cbor2.CBORTag) and dec.tag == 42 and dec.value == 'test-content' + += cbor2 encode semantic tag (tag 42), Scapy decode +enc = cbor2.dumps(cbor2.CBORTag(42, 'test-content')) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 42 and obj.val[1].val == 'test-content' and remainder == b'' + += Scapy and cbor2 produce identical bytes for semantic tag 42 +import cbor2 +scapy_enc = bytes(CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('test-content')))) +cbor2_enc = cbor2.dumps(cbor2.CBORTag(42, 'test-content')) +scapy_enc == cbor2_enc + += cbor2 encode epoch-based datetime tag (tag 1), Scapy decode +enc = cbor2.dumps(cbor2.CBORTag(1, 1363896240)) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 1 and obj.val[1].val == 1363896240 and remainder == b'' + += cbor2 encode integer-tagged byte string, Scapy decode +enc = cbor2.dumps(cbor2.CBORTag(100, b'\xde\xad\xbe\xef')) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 100 and obj.val[1].val == b'\xde\xad\xbe\xef' and remainder == b'' + ++ CBOR Interoperability - Half-Precision Floats (RFC 8949 vectors) + += Half-precision from RFC 8949: 0.0 +import cbor2 +data = bytes.fromhex('f90000') +obj, _ = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 0.0 + += Half-precision from RFC 8949: 1.0 +data = bytes.fromhex('f93c00') +obj, _ = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 1.0 + += Half-precision from RFC 8949: 1.5 +data = bytes.fromhex('f93e00') +obj, _ = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and obj.val == 1.5 + += Half-precision from RFC 8949: positive infinity +import math +data = bytes.fromhex('f97c00') +obj, _ = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 + += Half-precision from RFC 8949: NaN +data = bytes.fromhex('f97e00') +obj, _ = CBOR_Codecs.CBOR.dec(data) +isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) + += Scapy decode half-precision 1.5 agrees with cbor2 decode of double 1.5 +import cbor2 +half_data = bytes.fromhex('f93e00') +scapy_obj, _ = CBOR_Codecs.CBOR.dec(half_data) +double_data = bytes.fromhex('fb3ff8000000000000') +cbor2_val = cbor2.loads(double_data) +scapy_obj.val == cbor2_val + ++ CBOR Interoperability - Large Integers + += Large uint 18446744073709551615 bytes match cbor2 +import cbor2 +max_u64 = 18446744073709551615 +bytes(CBOR_UNSIGNED_INTEGER(max_u64)) == cbor2.dumps(max_u64) + += Large uint roundtrip Scapy to cbor2 to Scapy +max_u64 = 18446744073709551615 +scapy_enc = bytes(CBOR_UNSIGNED_INTEGER(max_u64)) +cbor2_val = cbor2.loads(scapy_enc) +cbor2_enc = cbor2.dumps(cbor2_val) +scapy_dec, _ = CBOR_Codecs.CBOR.dec(cbor2_enc) +scapy_dec.val == max_u64 + += Large negative int -18446744073709551616 roundtrip via cbor2 +neg_max = -18446744073709551616 +cbor2_enc = cbor2.dumps(neg_max) +scapy_dec, _ = CBOR_Codecs.CBOR.dec(cbor2_enc) +scapy_dec.val == neg_max + ++ CBOR Interoperability - Complex Nested Structures + += cbor2 deeply nested map: 3 levels, Scapy decode +import cbor2 +deep = {"level1": {"level2": {"level3": [1, 2, 3]}}} +enc = cbor2.dumps(deep) +obj, _ = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_MAP) and 'level1' in obj.val + += Scapy deeply nested array, cbor2 decode +from scapy.cbor.cborcodec import CBORcodec_ARRAY +enc = CBORcodec_ARRAY.enc([[1, [2, [3, [4]]]], 5]) +dec = cbor2.loads(enc) +dec == [[1, [2, [3, [4]]]], 5] + += cbor2 complex mixed structure: Scapy decodes it +import cbor2 +data = { + "name": "Alice", + "scores": [100, 95, 87], + "active": True, + "meta": {"created": 12345, "tag": "user"}, +} +enc = cbor2.dumps(data) +obj, _ = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_MAP) and 'name' in obj.val and 'scores' in obj.val + += Scapy encode complex structure, cbor2 decode, values match +from scapy.cbor.cborcodec import CBORcodec_MAP, CBORcodec_ARRAY +enc = CBORcodec_MAP.enc({ + "items": [1, 2, 3], + "count": 3, + "valid": True, +}) +dec = cbor2.loads(enc) +dec["items"] == [1, 2, 3] and dec["count"] == 3 and dec["valid"] is True + +########### CBORF Fields Interoperability Tests with cbor2 ############ + ++ CBORF Fields - Interop: CBORF_ARRAY packet to cbor2 + += CBORF_ARRAY packet to cbor2 list (version info) +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class VersionInfo(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER('major', 1), + CBORF_UNSIGNED_INTEGER('minor', 2), + CBORF_UNSIGNED_INTEGER('patch', 3), + ) + +pkt = VersionInfo() +raw = bytes(pkt) +dec = cbor2.loads(raw) +isinstance(dec, list) and dec == [1, 2, 3] + += cbor2 list to CBORF_ARRAY packet +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class VersionInfo2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER('major', 0), + CBORF_UNSIGNED_INTEGER('minor', 0), + CBORF_UNSIGNED_INTEGER('patch', 0), + ) + +cbor2_data = cbor2.dumps([4, 5, 6]) +pkt = VersionInfo2(cbor2_data) +pkt.major.val == 4 and pkt.minor.val == 5 and pkt.patch.val == 6 + += CBORF_ARRAY packet roundtrip through cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class MsgPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('code', 200), + CBORF_TEXT_STRING('status', 'ok'), + ) + +pkt = MsgPkt() +raw = bytes(pkt) +cbor2_dec = cbor2.loads(raw) +cbor2_re_enc = cbor2.dumps(cbor2_dec) +pkt2 = MsgPkt(cbor2_re_enc) +pkt2.code.val == 200 and pkt2.status.val == 'ok' + += CBORF_ARRAY with boolean and null fields to cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_BOOLEAN, CBORF_NULL, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class FlagPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('id', 7), + CBORF_BOOLEAN('active', True), + CBORF_NULL('reserved'), + ) + +pkt = FlagPkt() +raw = bytes(pkt) +dec = cbor2.loads(raw) +dec[0] == 7 and dec[1] is True and dec[2] is None + += cbor2 list with mixed types to CBORF_ARRAY packet +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_BOOLEAN, CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class Mixed(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('num', 0), + CBORF_BOOLEAN('flag', False), + CBORF_NULL('nval'), + ) + +cbor2_data = cbor2.dumps([42, False, None]) +pkt = Mixed(cbor2_data) +pkt.num.val == 42 + ++ CBORF Fields - Interop: CBORF_MAP packet to cbor2 + += CBORF_MAP packet to cbor2 dict +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class ClaimSet(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('iss', 'scapy'), + CBORF_INTEGER('exp', 9999999), + ) + +pkt = ClaimSet() +raw = bytes(pkt) +dec = cbor2.loads(raw) +isinstance(dec, dict) and dec.get('iss') == 'scapy' and dec.get('exp') == 9999999 + += cbor2 dict to CBORF_MAP packet +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class Claims(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('iss', ''), + CBORF_INTEGER('exp', 0), + ) + +cbor2_data = cbor2.dumps({'iss': 'myapp', 'exp': 12345}) +pkt = Claims(cbor2_data) +pkt.iss.val == 'myapp' and pkt.exp.val == 12345 + += CBORF_MAP packet roundtrip through cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class BinHeader(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('alg', 'ES256'), + CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), + ) + +pkt = BinHeader() +raw = bytes(pkt) +cbor2_dec = cbor2.loads(raw) +cbor2_re_enc = cbor2.dumps(cbor2_dec) +pkt2 = BinHeader(cbor2_re_enc) +pkt2.alg.val == 'ES256' and pkt2.kid.val == b'\x01\x02\x03\x04' + += CBORF_MAP with boolean values to cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class Flags(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_BOOLEAN('enabled', True), + CBORF_INTEGER('count', 5), + ) + +pkt = Flags() +raw = bytes(pkt) +dec = cbor2.loads(raw) +dec.get('enabled') is True and dec.get('count') == 5 + += cbor2 dict with unknown keys: CBORF_MAP skips them +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class SimpleMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('known', 'default'), + ) + +cbor2_data = cbor2.dumps({'known': 'value', 'unknown': 'extra'}) +pkt = SimpleMap(cbor2_data) +pkt.known.val == 'value' + ++ CBORF Fields - Interop: CBORF_ARRAY_OF packet to cbor2 + += CBORF_ARRAY_OF with integer elements to cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) + +pkt = IntList() +pkt.items = [CBOR_UNSIGNED_INTEGER(10), CBOR_UNSIGNED_INTEGER(20), CBOR_UNSIGNED_INTEGER(30)] +raw = bytes(pkt) +dec = cbor2.loads(raw) +isinstance(dec, list) and dec == [10, 20, 30] + += cbor2 list to CBORF_ARRAY_OF +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntList2(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) + +cbor2_data = cbor2.dumps([100, 200, 300]) +pkt = IntList2(cbor2_data) +len(pkt.items) == 3 and pkt.items[0].val == 100 and pkt.items[2].val == 300 + ++ CBORF Fields - Interop: CBORF_SEMANTIC_TAG to cbor2 + += CBORF_SEMANTIC_TAG packet to cbor2 CBORTag +import cbor2 +from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class TimestampPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag_info', None, 1, CBORF_UNSIGNED_INTEGER('ts', 1363896240)) + +pkt = TimestampPkt() +raw = bytes(pkt) +import datetime +dec = cbor2.loads(raw) +isinstance(dec, (cbor2.CBORTag, datetime.datetime, datetime.date)) + += cbor2 CBORTag (tag 42) decoded by Scapy CBOR_SEMANTIC_TAG +import cbor2 +enc = cbor2.dumps(cbor2.CBORTag(42, 'tagged-value')) +obj, remainder = CBOR_Codecs.CBOR.dec(enc) +isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 42 and obj.val[1].val == 'tagged-value' and remainder == b'' + += CBORF_SEMANTIC_TAG bytes identical to cbor2 CBORTag bytes +import cbor2 +scapy_enc = bytes(CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('tagged-value')))) +cbor2_enc = cbor2.dumps(cbor2.CBORTag(42, 'tagged-value')) +scapy_enc == cbor2_enc + ++ CBORF Fields - Interop: CBORF_UNSIGNED_INTEGER with cbor2 + += CBORF_UNSIGNED_INTEGER boundary values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class UIntPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) + +results = [] +for val in [0, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296, 18446744073709551615]: + pkt = UIntPkt() + pkt.n.val = val + dec = cbor2.loads(bytes(pkt)) + results.append(dec == val) + +all(results) + += CBORF_UNSIGNED_INTEGER boundary values - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class UIntPkt2(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) + +results = [] +for val in [0, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296]: + pkt = UIntPkt2(cbor2.dumps(val)) + results.append(pkt.n.val == val) + +all(results) + += CBORF_UNSIGNED_INTEGER byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class UIntExact(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) + +results = [] +for val in [0, 1, 10, 23, 24, 255, 256, 65535, 65536, 4294967295]: + pkt = UIntExact() + pkt.n.val = val + results.append(bytes(pkt) == cbor2.dumps(val)) + +all(results) + ++ CBORF Fields - Interop: CBORF_NEGATIVE_INTEGER with cbor2 + += CBORF_NEGATIVE_INTEGER boundary values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class NIntPkt(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) + +results = [] +for val in [-1, -24, -25, -256, -257, -65536, -65537, -4294967296, -4294967297]: + pkt = NIntPkt() + pkt.n.val = val + dec = cbor2.loads(bytes(pkt)) + results.append(dec == val) + +all(results) + += CBORF_NEGATIVE_INTEGER boundary values - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class NIntPkt2(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) + +results = [] +for val in [-1, -24, -25, -256, -257, -65536, -4294967296]: + pkt = NIntPkt2(cbor2.dumps(val)) + results.append(pkt.n.val == val) + +all(results) + += CBORF_NEGATIVE_INTEGER byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +from scapy.cborpacket import CBOR_Packet + +class NIntExact(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) + +results = [] +for val in [-1, -10, -24, -25, -256, -257, -65536, -65537]: + pkt = NIntExact() + pkt.n.val = val + results.append(bytes(pkt) == cbor2.dumps(val)) + +all(results) + ++ CBORF Fields - Interop: CBORF_INTEGER with cbor2 + += CBORF_INTEGER positive values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntPkt(CBOR_Packet): + CBOR_root = CBORF_INTEGER('n', 0) + +results = [] +for val in [0, 1, 42, 100, 1000, 1000000]: + pkt = IntPkt() + pkt.n.val = val + dec = cbor2.loads(bytes(pkt)) + results.append(dec == val) + +all(results) + += CBORF_INTEGER negative values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntNegPkt(CBOR_Packet): + CBOR_root = CBORF_INTEGER('n', -1) + +results = [] +for val in [-1, -10, -100, -1000, -1000000]: + pkt = IntNegPkt() + pkt.n.val = val + dec = cbor2.loads(bytes(pkt)) + results.append(dec == val) + +all(results) + += CBORF_INTEGER - cbor2 encode positive and negative, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntPkt2(CBOR_Packet): + CBOR_root = CBORF_INTEGER('n', 0) + +results = [] +for val in [0, 42, -1, -42, 255, -256, 65536, -65537]: + pkt = IntPkt2(cbor2.dumps(val)) + results.append(pkt.n.val == val) + +all(results) + ++ CBORF Fields - Interop: CBORF_BYTE_STRING with cbor2 + += CBORF_BYTE_STRING empty bytes - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class BytePkt(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING('data', b'') + +pkt = BytePkt() +dec = cbor2.loads(bytes(pkt)) +dec == b'' + += CBORF_BYTE_STRING all 256 byte values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class ByteAllPkt(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING('data', b'') + +pkt = ByteAllPkt() +pkt.data.val = bytes(range(256)) +dec = cbor2.loads(bytes(pkt)) +dec == bytes(range(256)) + += CBORF_BYTE_STRING - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class BytePkt3(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING('data', b'') + +for raw_val in [b'', b'\xde\xad\xbe\xef', bytes(range(256))]: + pkt = BytePkt3(cbor2.dumps(raw_val)) + assert pkt.data.val == raw_val + +True + += CBORF_BYTE_STRING byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class ByteExact(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING('data', b'') + +results = [] +for raw_val in [b'', b'\x00', b'\xff', b'\xde\xad\xbe\xef', b'hello']: + pkt = ByteExact() + pkt.data.val = raw_val + results.append(bytes(pkt) == cbor2.dumps(raw_val)) + +all(results) + ++ CBORF Fields - Interop: CBORF_TEXT_STRING with cbor2 + += CBORF_TEXT_STRING empty string - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextPkt(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING('txt', '') + +pkt = TextPkt() +dec = cbor2.loads(bytes(pkt)) +dec == '' + += CBORF_TEXT_STRING ASCII string - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextPkt2(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING('txt', '') + +pkt = TextPkt2() +pkt.txt.val = 'Hello, World!' +dec = cbor2.loads(bytes(pkt)) +dec == 'Hello, World!' + += CBORF_TEXT_STRING unicode string - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextUniPkt(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING('txt', '') + +pkt = TextUniPkt() +pkt.txt.val = u'Hello, \u4e16\u754c' +dec = cbor2.loads(bytes(pkt)) +dec == u'Hello, \u4e16\u754c' + += CBORF_TEXT_STRING - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextPkt3(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING('txt', '') + +for s in ['', 'hello', 'Hello, World!', u'caf\u00e9', u'\u4e16\u754c']: + pkt = TextPkt3(cbor2.dumps(s)) + assert pkt.txt.val == s + +True + += CBORF_TEXT_STRING byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextExact(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING('txt', '') + +results = [] +for s in ['', 'a', 'hello', 'IETF', u'\u6c34']: + pkt = TextExact() + pkt.txt.val = s + results.append(bytes(pkt) == cbor2.dumps(s)) + +all(results) + ++ CBORF Fields - Interop: CBORF_BOOLEAN with cbor2 + += CBORF_BOOLEAN true - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class BoolPkt(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN('flag', True) + +pkt = BoolPkt() +dec = cbor2.loads(bytes(pkt)) +dec is True + += CBORF_BOOLEAN false - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class BoolFalsePkt(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN('flag', False) + +pkt = BoolFalsePkt() +dec = cbor2.loads(bytes(pkt)) +dec is False + += CBORF_BOOLEAN - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class BoolPkt2(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN('flag', False) + +pkt_true = BoolPkt2(cbor2.dumps(True)) +pkt_false = BoolPkt2(cbor2.dumps(False)) +pkt_true.flag.val is True and pkt_false.flag.val is False + += CBORF_BOOLEAN byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class BoolExactTrue(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN('flag', True) + +class BoolExactFalse(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN('flag', False) + +pkt_t = BoolExactTrue() +pkt_f = BoolExactFalse() +bytes(pkt_t) == cbor2.dumps(True) and bytes(pkt_f) == cbor2.dumps(False) + ++ CBORF Fields - Interop: CBORF_NULL with cbor2 + += CBORF_NULL - Scapy encode, cbor2 decode gives None +import cbor2 +from scapy.cbor.cborfields import CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class NullPkt(CBOR_Packet): + CBOR_root = CBORF_NULL('n') + +pkt = NullPkt() +dec = cbor2.loads(bytes(pkt)) +dec is None + += CBORF_NULL byte-exact comparison with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class NullExact(CBOR_Packet): + CBOR_root = CBORF_NULL('n') + +pkt = NullExact() +bytes(pkt) == cbor2.dumps(None) + += CBORF_NULL - cbor2 None encode, Scapy decode gives CBOR_NULL +import cbor2 +from scapy.cbor.cbor import CBOR_NULL +from scapy.cbor.cborfields import CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class NullPkt2(CBOR_Packet): + CBOR_root = CBORF_NULL('n') + +pkt = NullPkt2(cbor2.dumps(None)) +isinstance(pkt.n, CBOR_NULL) + ++ CBORF Fields - Interop: CBORF_FLOAT with cbor2 + += CBORF_FLOAT basic values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class FloatPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT('f', 0.0) + +results = [] +for val in [0.0, 1.0, -1.0, 3.14159, 1e10, -2.5]: + pkt = FloatPkt() + pkt.f.val = val + dec = cbor2.loads(bytes(pkt)) + results.append(dec == val) + +all(results) + += CBORF_FLOAT special values (NaN, Inf, -Inf) - Scapy encode, cbor2 decode +import cbor2, math +from scapy.cbor.cborfields import CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class FloatSpecialPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT('f', 0.0) + +pkt_nan = FloatSpecialPkt() +pkt_nan.f.val = float('nan') +raw_nan = bytes(pkt_nan) +pkt_inf = FloatSpecialPkt() +pkt_inf.f.val = float('inf') +raw_inf = bytes(pkt_inf) +pkt_ninf = FloatSpecialPkt() +pkt_ninf.f.val = float('-inf') +raw_ninf = bytes(pkt_ninf) +math.isnan(cbor2.loads(raw_nan)) and math.isinf(cbor2.loads(raw_inf)) and cbor2.loads(raw_ninf) == float('-inf') + += CBORF_FLOAT special values - cbor2 encode, Scapy decode +import cbor2, math +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class FloatArrPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_FLOAT('nan_val', 0.0), + CBORF_FLOAT('inf_val', 0.0), + CBORF_FLOAT('ninf_val', 0.0), + ) + +pkt = FloatArrPkt(cbor2.dumps([float('nan'), float('inf'), float('-inf')])) +math.isnan(pkt.nan_val.val) and math.isinf(pkt.inf_val.val) and pkt.ninf_val.val == float('-inf') + += CBORF_FLOAT - cbor2 encode, Scapy decode roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class FloatPkt2(CBOR_Packet): + CBOR_root = CBORF_FLOAT('f', 0.0) + +results = [] +for val in [0.0, 1.0, -1.0, 2.5, 100.0]: + pkt = FloatPkt2(cbor2.dumps(val)) + results.append(pkt.f.val == val) + +all(results) + ++ CBORF Fields - Interop: CBORF_ARRAY with cbor2 + += CBORF_ARRAY with integer fields - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class PointPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('x', 10), + CBORF_INTEGER('y', 20), + CBORF_INTEGER('z', 30), + ) + +pkt = PointPkt() +dec = cbor2.loads(bytes(pkt)) +dec == [10, 20, 30] + += CBORF_ARRAY with mixed types - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class MixedPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('id', 99), + CBORF_TEXT_STRING('label', 'test'), + CBORF_BOOLEAN('active', True), + ) + +pkt = MixedPkt() +dec = cbor2.loads(bytes(pkt)) +dec[0] == 99 and dec[1] == 'test' and dec[2] is True + += CBORF_ARRAY - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class RecordPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('code', 0), + CBORF_TEXT_STRING('msg', ''), + ) + +pkt = RecordPkt(cbor2.dumps([200, 'OK'])) +pkt.code.val == 200 and pkt.msg.val == 'OK' + += CBORF_ARRAY roundtrip through cbor2 - multiple encode/decode cycles +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class RTPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('seq', 1), + CBORF_TEXT_STRING('data', 'payload'), + ) + +pkt = RTPkt() +raw = bytes(pkt) +cbor2_dec = cbor2.loads(raw) +re_enc = cbor2.dumps(cbor2_dec) +pkt2 = RTPkt(re_enc) +pkt2.seq.val == 1 and pkt2.data.val == 'payload' + += CBORF_ARRAY with null elements - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class NullArrPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('id', 5), + CBORF_NULL('opt'), + ) + +pkt = NullArrPkt() +dec = cbor2.loads(bytes(pkt)) +dec[0] == 5 and dec[1] is None + ++ CBORF Fields - Interop: CBORF_ARRAY_OF with cbor2 + += CBORF_ARRAY_OF with text strings - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cbor import CBOR_TEXT_STRING +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextListPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) + +pkt = TextListPkt(cbor2.dumps(['hello', 'world', 'foo'])) +len(pkt.items) == 3 and pkt.items[0].val == 'hello' and pkt.items[2].val == 'foo' + += CBORF_ARRAY_OF with text strings - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cbor import CBOR_TEXT_STRING +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextListPkt2(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) + +pkt = TextListPkt2() +pkt.items = [CBOR_TEXT_STRING('abc'), CBOR_TEXT_STRING('def'), CBOR_TEXT_STRING('ghi')] +dec = cbor2.loads(bytes(pkt)) +dec == ['abc', 'def', 'ghi'] + += CBORF_ARRAY_OF with text strings roundtrip through cbor2 +import cbor2 +from scapy.cbor.cbor import CBOR_TEXT_STRING +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class TextListRT(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) + +pkt = TextListRT() +pkt.items = [CBOR_TEXT_STRING('x'), CBOR_TEXT_STRING('y'), CBOR_TEXT_STRING('z')] +raw = bytes(pkt) +re_enc = cbor2.dumps(cbor2.loads(raw)) +pkt2 = TextListRT(re_enc) +len(pkt2.items) == 3 and pkt2.items[1].val == 'y' + += CBORF_ARRAY_OF with byte strings - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cbor import CBOR_BYTE_STRING +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class ByteListPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) + +pkt = ByteListPkt(cbor2.dumps([b'\x01\x02', b'\x03\x04', b'\x05\x06'])) +len(pkt.items) == 3 and pkt.items[0].val == b'\x01\x02' and pkt.items[2].val == b'\x05\x06' + += CBORF_ARRAY_OF with byte strings - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cbor import CBOR_BYTE_STRING +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class ByteListPkt2(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) + +pkt = ByteListPkt2() +pkt.items = [CBOR_BYTE_STRING(b'\xaa\xbb'), CBOR_BYTE_STRING(b'\xcc\xdd')] +dec = cbor2.loads(bytes(pkt)) +dec == [b'\xaa\xbb', b'\xcc\xdd'] + += CBORF_ARRAY_OF integers - large list cbor2 roundtrip +import cbor2 +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class BigIntList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) + +cbor2_data = cbor2.dumps(list(range(50))) +pkt = BigIntList(cbor2_data) +len(pkt.items) == 50 and pkt.items[0].val == 0 and pkt.items[49].val == 49 + += CBORF_ARRAY_OF integers - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class IntListPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) + +pkt = IntListPkt() +pkt.items = [CBOR_UNSIGNED_INTEGER(i) for i in [10, 20, 30, 40, 50]] +dec = cbor2.loads(bytes(pkt)) +dec == [10, 20, 30, 40, 50] + ++ CBORF Fields - Interop: CBORF_MAP with cbor2 + += CBORF_MAP with text string values - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class HeaderPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('alg', 'ES256'), + CBORF_TEXT_STRING('typ', 'JWT'), + CBORF_INTEGER('ver', 1), + ) + +pkt = HeaderPkt() +dec = cbor2.loads(bytes(pkt)) +isinstance(dec, dict) and dec.get('alg') == 'ES256' and dec.get('typ') == 'JWT' and dec.get('ver') == 1 + += CBORF_MAP - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class CredPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('sub', ''), + CBORF_INTEGER('iat', 0), + CBORF_BOOLEAN('admin', False), + ) + +pkt = CredPkt(cbor2.dumps({'sub': 'user42', 'iat': 1700000000, 'admin': True})) +pkt.sub.val == 'user42' and pkt.iat.val == 1700000000 and pkt.admin.val is True + += CBORF_MAP roundtrip through cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class CoseHeaderPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('alg', 'ES256'), + CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), + CBORF_INTEGER('crit', 1), + ) + +pkt = CoseHeaderPkt() +raw = bytes(pkt) +re_enc = cbor2.dumps(cbor2.loads(raw)) +pkt2 = CoseHeaderPkt(re_enc) +pkt2.alg.val == 'ES256' and pkt2.kid.val == b'\x01\x02\x03\x04' and pkt2.crit.val == 1 + += CBORF_MAP with null value - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_NULL +from scapy.cborpacket import CBOR_Packet + +class OptionalPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('id', 7), + CBORF_NULL('optional_data'), + ) + +pkt = OptionalPkt() +dec = cbor2.loads(bytes(pkt)) +dec.get('id') == 7 and dec.get('optional_data') is None + += CBORF_MAP with boolean values roundtrip with cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class FlagsPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_BOOLEAN('active', True), + CBORF_BOOLEAN('verified', False), + CBORF_INTEGER('level', 3), + CBORF_TEXT_STRING('role', 'admin'), + ) + +pkt = FlagsPkt() +raw = bytes(pkt) +dec = cbor2.loads(raw) +re_enc = cbor2.dumps(dec) +pkt2 = FlagsPkt(re_enc) +pkt2.active.val is True and pkt2.verified.val is False and pkt2.level.val == 3 and pkt2.role.val == 'admin' + += CBORF_MAP skip unknown keys from cbor2 +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class KnownKeysPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('known', 'default'), + CBORF_INTEGER('count', 0), + ) + +pkt = KnownKeysPkt(cbor2.dumps({'known': 'found', 'count': 42, 'extra': 'ignored'})) +pkt.known.val == 'found' and pkt.count.val == 42 + ++ CBORF Fields - Interop: CBOR_Packet complex structures with cbor2 + += CBOR_Packet CBORF_ARRAY with multiple field types - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class SensorReading(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('sensor_id', 42), + CBORF_TEXT_STRING('unit', 'fahrenheit'), + CBORF_INTEGER('value', 98), + CBORF_BOOLEAN('alarm', True), + ) + +pkt = SensorReading() +dec = cbor2.loads(bytes(pkt)) +dec[0] == 42 and dec[1] == 'fahrenheit' and dec[2] == 98 and dec[3] is True + += CBOR_Packet with CBORF_MAP multiple field types - Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class DeviceInfo(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('id', 0), + CBORF_TEXT_STRING('label', ''), + CBORF_BOOLEAN('online', False), + CBORF_BYTE_STRING('hwaddr', b''), + ) + +pkt = DeviceInfo(cbor2.dumps({'id': 1001, 'label': 'device-01', 'online': True, 'hwaddr': b'\x00\x11\x22\x33\x44\x55'})) +dec = cbor2.loads(bytes(pkt)) +dec.get('id') == 1001 and dec.get('label') == 'device-01' and dec.get('online') is True and dec.get('hwaddr') == b'\x00\x11\x22\x33\x44\x55' + += CBOR_Packet CBORF_MAP full cbor2 roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN +from scapy.cborpacket import CBOR_Packet + +class ClaimsPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('iss', ''), + CBORF_TEXT_STRING('sub', ''), + CBORF_INTEGER('exp', 0), + CBORF_BOOLEAN('admin', False), + ) + +pkt = ClaimsPkt(cbor2.dumps({'iss': 'auth.example.com', 'sub': 'user99', 'exp': 9999999, 'admin': False})) +raw = bytes(pkt) +dec = cbor2.loads(raw) +dec.get('iss') == 'auth.example.com' and dec.get('sub') == 'user99' and dec.get('exp') == 9999999 and dec.get('admin') is False + += CBOR_Packet CBORF_MAP with negative integer - cbor2 encode, Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class OffsetPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('name', ''), + CBORF_INTEGER('offset', 0), + CBORF_INTEGER('count', 0), + ) + +pkt = OffsetPkt(cbor2.dumps({'name': 'delta', 'offset': -1024, 'count': 512})) +pkt.name.val == 'delta' and pkt.offset.val == -1024 and pkt.count.val == 512 + ++ CBOR_Packet - nested CBORF_PACKET structures + += CBORF_PACKET three levels deep: Outer(ARRAY) -> Middle(ARRAY) -> Inner(ARRAY) +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class NestInner(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('x', 0), + CBORF_INTEGER('y', 0), + ) + +class NestMiddle(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_TEXT_STRING('zone', ''), + CBORF_PACKET('point', None, NestInner), + ) + +class NestOuter(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('version', 0), + CBORF_PACKET('region', None, NestMiddle), + ) + +inner = NestInner(cbor2.dumps([30, 40])) +mid = NestMiddle() +mid.zone.val = 'north' +mid.point = inner +outer = NestOuter() +outer.version.val = 2 +outer.region = mid +raw = bytes(outer) +outer2 = NestOuter(raw) +outer2.version.val == 2 and outer2.region.zone.val == 'north' and outer2.region.point.x.val == 30 and outer2.region.point.y.val == 40 + += CBORF_PACKET three-level nesting cbor2 interop +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class NestInner2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('x', 0), + CBORF_INTEGER('y', 0), + ) + +class NestMiddle2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_TEXT_STRING('zone', ''), + CBORF_PACKET('point', None, NestInner2), + ) + +class NestOuter2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('version', 0), + CBORF_PACKET('region', None, NestMiddle2), + ) + +inner = NestInner2(cbor2.dumps([10, 20])) +mid = NestMiddle2() +mid.zone.val = 'south' +mid.point = inner +outer = NestOuter2() +outer.version.val = 1 +outer.region = mid +dec = cbor2.loads(bytes(outer)) +dec == [1, ['south', [10, 20]]] + += CBORF_PACKET inside CBORF_MAP: cbor2 decode matches field values +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_MAP, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class MapInner(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('px', 0), + CBORF_INTEGER('py', 0), + ) + +class MapWithNestedPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('label', ''), + CBORF_PACKET('coords', None, MapInner), + ) + +inner = MapInner(cbor2.dumps([5, 7])) +pkt = MapWithNestedPkt() +pkt.label.val = 'origin' +pkt.coords = inner +dec = cbor2.loads(bytes(pkt)) +dec.get('label') == 'origin' and dec.get('coords') == [5, 7] + += CBORF_PACKET inside CBORF_MAP: Scapy decode roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_MAP, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class CoordsInner(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('px', 0), + CBORF_INTEGER('py', 0), + ) + +class CoordsOuter(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('label', ''), + CBORF_PACKET('coords', None, CoordsInner), + ) + +inner = CoordsInner(cbor2.dumps([5, 7])) +pkt = CoordsOuter() +pkt.label.val = 'origin' +pkt.coords = inner +pkt2 = CoordsOuter(bytes(pkt)) +pkt2.label.val == 'origin' and pkt2.coords.px.val == 5 and pkt2.coords.py.val == 7 + += CBORF_PACKET: nested MAP-in-MAP via CBORF_PACKET (Document/Metadata) +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class DocMeta(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('creator', ''), + CBORF_INTEGER('version', 0), + ) + +class DocPacket(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('title', ''), + CBORF_BYTE_STRING('body', b''), + CBORF_PACKET('metadata', None, DocMeta), + ) + +meta = DocMeta() +meta.creator.val = 'alice' +meta.version.val = 3 +doc = DocPacket() +doc.title.val = 'My Document' +doc.body.val = b'hello world' +doc.metadata = meta +raw = bytes(doc) +dec = cbor2.loads(raw) +dec.get('title') == 'My Document' and dec.get('body') == b'hello world' and dec.get('metadata') == {'creator': 'alice', 'version': 3} + += CBORF_PACKET: nested MAP-in-MAP Scapy roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class DocMeta2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('creator', ''), + CBORF_INTEGER('version', 0), + ) + +class DocPacket2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('title', ''), + CBORF_BYTE_STRING('body', b''), + CBORF_PACKET('metadata', None, DocMeta2), + ) + +meta = DocMeta2() +meta.creator.val = 'bob' +meta.version.val = 7 +doc = DocPacket2() +doc.title.val = 'Report' +doc.body.val = b'\x01\x02\x03' +doc.metadata = meta +raw = bytes(doc) +doc2 = DocPacket2(raw) +doc2.title.val == 'Report' and doc2.body.val == b'\x01\x02\x03' and doc2.metadata.creator.val == 'bob' and doc2.metadata.version.val == 7 + ++ CBOR_Packet - CBORF_ARRAY_OF with CBOR_Packet elements + += CBORF_ARRAY_OF with CBOR_Packet class: cbor2 list of lists → Scapy decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +from scapy.cborpacket import CBOR_Packet + +class StatusItem(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('code', 0), + CBORF_TEXT_STRING('msg', ''), + ) + +class StatusList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('statuses', [], StatusItem) + +raw = cbor2.dumps([[200, 'OK'], [201, 'Created'], [204, 'No Content']]) +pkt = StatusList(raw) +len(pkt.statuses) == 3 and pkt.statuses[0].code.val == 200 and pkt.statuses[1].msg.val == 'Created' and pkt.statuses[2].code.val == 204 + += CBORF_ARRAY_OF with CBOR_Packet class: Scapy encode → cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +from scapy.cborpacket import CBOR_Packet + +class ErrItem(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('code', 0), + CBORF_TEXT_STRING('msg', ''), + ) + +class ErrList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('errors', [], ErrItem) + +pkt = ErrList() +pkt.errors = [ErrItem(cbor2.dumps([404, 'Not Found'])), ErrItem(cbor2.dumps([500, 'Server Error']))] +dec = cbor2.loads(bytes(pkt)) +dec == [[404, 'Not Found'], [500, 'Server Error']] + += CBORF_ARRAY_OF with CBOR_Packet class: roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +from scapy.cborpacket import CBOR_Packet + +class MsgItem(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('id', 0), + CBORF_TEXT_STRING('txt', ''), + ) + +class MsgList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('messages', [], MsgItem) + +raw = cbor2.dumps([[1, 'hello'], [2, 'world'], [3, 'foo']]) +pkt = MsgList(raw) +raw2 = bytes(pkt) +pkt2 = MsgList(raw2) +len(pkt2.messages) == 3 and pkt2.messages[2].id.val == 3 and pkt2.messages[2].txt.val == 'foo' + += CBORF_ARRAY_OF with CBOR_Packet class: empty list +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +from scapy.cborpacket import CBOR_Packet + +class EmptyItem(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('val', 0), + ) + +class EmptyItemList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('items', [], EmptyItem) + +pkt = EmptyItemList() +raw = bytes(pkt) +dec = cbor2.loads(raw) +dec == [] and len(EmptyItemList(raw).items) == 0 + += CBORF_ARRAY_OF with CBOR_Packet class inside CBORF_MAP +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF, CBORF_MAP, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class EventItem(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_TEXT_STRING('evt', ''), + CBORF_INTEGER('ts', 0), + ) + +class EventLog(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('events', [], EventItem) + +class Report(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('source', ''), + CBORF_INTEGER('count', 0), + CBORF_PACKET('log', None, EventLog), + ) + +log = EventLog() +log.events = [EventItem(cbor2.dumps(['boot', 1000])), EventItem(cbor2.dumps(['login', 2000]))] +rpt = Report() +rpt.source.val = 'sensor-1' +rpt.count.val = 2 +rpt.log = log +raw = bytes(rpt) +dec = cbor2.loads(raw) +dec.get('source') == 'sensor-1' and dec.get('count') == 2 and dec.get('log') == [['boot', 1000], ['login', 2000]] + ++ CBOR_Packet - CBORF_optional extended tests + += CBORF_optional: type mismatch in CBORF_ARRAY sets field to None +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class TwoFieldPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('version', 0), + CBORF_optional(CBORF_TEXT_STRING('description', 'none')), + ) + +raw = cbor2.dumps([7, 99]) +pkt = TwoFieldPkt(raw) +pkt.version.val == 7 and pkt.description is None + += CBORF_optional: correct type present is decoded normally +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class OptPresentPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('version', 0), + CBORF_optional(CBORF_TEXT_STRING('description', '')), + ) + +raw = cbor2.dumps([3, 'hello world']) +pkt = OptPresentPkt(raw) +pkt.version.val == 3 and pkt.description.val == 'hello world' + += CBORF_optional: encode and decode roundtrip with present field +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class OptRTPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('version', 1), + CBORF_optional(CBORF_TEXT_STRING('title', '')), + ) + +pkt = OptRTPkt() +pkt.title.val = 'test title' +raw = bytes(pkt) +pkt2 = OptRTPkt(raw) +pkt2.version.val == 1 and pkt2.title.val == 'test title' + += CBORF_optional: cbor2 interop - optional present +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class OptInteropPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('seq', 0), + CBORF_optional(CBORF_TEXT_STRING('note', '')), + ) + +pkt = OptInteropPkt() +pkt.note.val = 'cbor2 interop' +dec = cbor2.loads(bytes(pkt)) +dec == [0, 'cbor2 interop'] + += CBORF_optional inside CBORF_MAP: key present in cbor2 dict is decoded +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class ConfigWithOpt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('timeout', 30), + CBORF_optional(CBORF_TEXT_STRING('endpoint', '')), + CBORF_INTEGER('retries', 3), + ) + +pkt = ConfigWithOpt(cbor2.dumps({'timeout': 60, 'endpoint': 'https://example.com', 'retries': 5})) +pkt.timeout.val == 60 and pkt.endpoint.val == 'https://example.com' and pkt.retries.val == 5 + += CBORF_optional inside CBORF_MAP: missing key stays at default +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +from scapy.cborpacket import CBOR_Packet + +class ConfigNoOpt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('timeout', 30), + CBORF_optional(CBORF_TEXT_STRING('endpoint', '')), + CBORF_INTEGER('retries', 3), + ) + +pkt = ConfigNoOpt(cbor2.dumps({'timeout': 15, 'retries': 2})) +pkt.timeout.val == 15 and pkt.retries.val == 2 + ++ CBOR_Packet - CBORF_SEMANTIC_TAG extended tests + += CBORF_SEMANTIC_TAG with TEXT_STRING inner: Scapy encode, cbor2 decode as datetime +import cbor2, datetime +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_TEXT_STRING, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class DatetimePkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 0, CBORF_TEXT_STRING('dt', '')) + +pkt = DatetimePkt() +pkt.dt.val = '2023-01-15T12:00:00Z' +dec = cbor2.loads(bytes(pkt)) +isinstance(dec, datetime.datetime) + += CBORF_SEMANTIC_TAG with INTEGER inner: Scapy encode, cbor2 decode as datetime +import cbor2, datetime +from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class UnixTimePkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) + +pkt = UnixTimePkt() +pkt.ts.val = 1700000000 +dec = cbor2.loads(bytes(pkt)) +isinstance(dec, datetime.datetime) + += CBORF_SEMANTIC_TAG roundtrip: Scapy encode → Scapy decode preserves inner value +import cbor2 +from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class TagRTPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) + +pkt = TagRTPkt() +pkt.ts.val = 1700000000 +raw = bytes(pkt) +pkt2 = TagRTPkt(raw) +pkt2.ts.val == 1700000000 + += CBORF_SEMANTIC_TAG: tag byte matches CBOR major type 6 encoding +import cbor2 +from scapy.cbor.cborfields import CBORF_BYTE_STRING, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class TagBigNum(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 2, CBORF_BYTE_STRING('n', b'')) + +pkt = TagBigNum() +pkt.n.val = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00' +raw = bytes(pkt) +raw[0:1] == b'\xc2' + += CBORF_SEMANTIC_TAG: byte-exact comparison with cbor2 CBORTag +import cbor2 +from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class TagCmpPkt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) + +pkt = TagCmpPkt() +pkt.ts.val = 9999999 +bytes(pkt) == cbor2.dumps(cbor2.CBORTag(1, 9999999)) + += CBORF_SEMANTIC_TAG inside CBORF_MAP: Scapy encode, cbor2 decode +import cbor2, datetime +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class EventPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('event_type', ''), + CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), + ) + +pkt = EventPkt() +pkt.event_type.val = 'login' +pkt.ts.val = 9999999 +dec = cbor2.loads(bytes(pkt)) +isinstance(dec, dict) and dec.get('event_type') == 'login' and isinstance(dec.get('tag'), datetime.datetime) + += CBORF_SEMANTIC_TAG inside CBORF_MAP: Scapy roundtrip preserves inner value +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class EventRTPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_TEXT_STRING('event_type', ''), + CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), + ) + +pkt = EventRTPkt() +pkt.event_type.val = 'logout' +pkt.ts.val = 1234567890 +raw = bytes(pkt) +pkt2 = EventRTPkt(raw) +pkt2.event_type.val == 'logout' and pkt2.ts.val == 1234567890 + += CBORF_SEMANTIC_TAG inside CBORF_ARRAY: Scapy encode, cbor2 decode +import cbor2, datetime +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG +from scapy.cborpacket import CBOR_Packet + +class TimedEventArr(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_TEXT_STRING('evt', ''), + CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), + ) + +pkt = TimedEventArr() +pkt.evt.val = 'start' +pkt.ts.val = 1700000000 +dec = cbor2.loads(bytes(pkt)) +dec[0] == 'start' and isinstance(dec[1], datetime.datetime) + ++ CBOR_Packet - realistic models + += Realistic model: EAT-like attestation token +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class EATToken(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('nonce', 0), + CBORF_TEXT_STRING('ueid', ''), + CBORF_BYTE_STRING('boot_seed', b''), + CBORF_INTEGER('hwver', 0), + ) + +raw = cbor2.dumps({'nonce': 12345, 'ueid': 'device-abc', 'boot_seed': b'\x00' * 16, 'hwver': 3}) +pkt = EATToken(raw) +pkt.nonce.val == 12345 and pkt.ueid.val == 'device-abc' and pkt.boot_seed.val == b'\x00' * 16 and pkt.hwver.val == 3 + += Realistic model: EAT-like token Scapy encode, cbor2 decode +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class EATToken2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('nonce', 0), + CBORF_TEXT_STRING('ueid', ''), + CBORF_BYTE_STRING('boot_seed', b''), + CBORF_INTEGER('hwver', 0), + ) + +pkt = EATToken2() +pkt.nonce.val = 99999 +pkt.ueid.val = 'iot-sensor-01' +pkt.boot_seed.val = b'\xde\xad\xbe\xef' * 4 +pkt.hwver.val = 5 +dec = cbor2.loads(bytes(pkt)) +dec.get('nonce') == 99999 and dec.get('ueid') == 'iot-sensor-01' and dec.get('boot_seed') == b'\xde\xad\xbe\xef' * 4 and dec.get('hwver') == 5 + += Realistic model: SensorReport with CBORF_PACKET inner reading +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_FLOAT, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class SensorData(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('sensor_id', 0), + CBORF_FLOAT('temperature', 0.0), + ) + +class SensorReport(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('station', 0), + CBORF_TEXT_STRING('unit', ''), + CBORF_PACKET('reading', None, SensorData), + ) + +reading = SensorData() +reading.sensor_id.val = 3 +reading.temperature.val = 98.6 +rpt = SensorReport() +rpt.station.val = 5 +rpt.unit.val = 'fahrenheit' +rpt.reading = reading +dec = cbor2.loads(bytes(rpt)) +dec.get('station') == 5 and dec.get('unit') == 'fahrenheit' and dec.get('reading') == [3, 98.6] + += Realistic model: SensorReport Scapy roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_FLOAT, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet + +class SensorData2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('sensor_id', 0), + CBORF_FLOAT('temperature', 0.0), + ) + +class SensorReport2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('station', 0), + CBORF_TEXT_STRING('unit', ''), + CBORF_PACKET('reading', None, SensorData2), + ) + +raw = cbor2.dumps({'station': 9, 'unit': 'celsius', 'reading': [7, 36.5]}) +pkt = SensorReport2(raw) +pkt2 = SensorReport2(bytes(pkt)) +pkt2.station.val == 9 and pkt2.unit.val == 'celsius' and pkt2.reading.sensor_id.val == 7 + += Realistic model: StatusList (CBORF_ARRAY_OF of CBOR_Packets) encode and decode +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +from scapy.cborpacket import CBOR_Packet + +class HttpStatus(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('code', 0), + CBORF_TEXT_STRING('phrase', ''), + ) + +class HttpStatusList(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF('statuses', [], HttpStatus) + +raw = cbor2.dumps([[200, 'OK'], [201, 'Created'], [404, 'Not Found']]) +pkt = HttpStatusList(raw) +raw2 = bytes(pkt) +dec = cbor2.loads(raw2) +dec == [[200, 'OK'], [201, 'Created'], [404, 'Not Found']] + += Realistic model: HTTP response header map +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class HttpResponse(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('status', 0), + CBORF_TEXT_STRING('content_type', ''), + CBORF_INTEGER('content_length', 0), + CBORF_BYTE_STRING('body', b''), + ) + +pkt = HttpResponse() +pkt.status.val = 200 +pkt.content_type.val = 'application/cbor' +pkt.content_length.val = 4 +pkt.body.val = b'\x01\x02\x03\x04' +dec = cbor2.loads(bytes(pkt)) +dec.get('status') == 200 and dec.get('content_type') == 'application/cbor' and dec.get('body') == b'\x01\x02\x03\x04' + += Realistic model: HTTP response header cbor2 → Scapy roundtrip +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +from scapy.cborpacket import CBOR_Packet + +class HttpResponse2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('status', 0), + CBORF_TEXT_STRING('content_type', ''), + CBORF_INTEGER('content_length', 0), + CBORF_BYTE_STRING('body', b''), + ) + +raw = cbor2.dumps({'status': 404, 'content_type': 'text/plain', 'content_length': 9, 'body': b'Not Found'}) +pkt = HttpResponse2(raw) +pkt2 = HttpResponse2(bytes(pkt)) +pkt2.status.val == 404 and pkt2.content_type.val == 'text/plain' and pkt2.body.val == b'Not Found' + += Realistic model: COSE-like header map with integer algorithm +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_BYTE_STRING, CBORF_TEXT_STRING +from scapy.cborpacket import CBOR_Packet + +class CoseHeader(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('alg', 0), + CBORF_TEXT_STRING('kid', ''), + CBORF_BYTE_STRING('x5t', b''), + ) + +pkt = CoseHeader(cbor2.dumps({'alg': -7, 'kid': 'key-42', 'x5t': b'\xaa\xbb\xcc\xdd'})) +dec = cbor2.loads(bytes(pkt)) +dec.get('alg') == -7 and dec.get('kid') == 'key-42' and dec.get('x5t') == b'\xaa\xbb\xcc\xdd' + += Realistic model: CBOR_Packet fields_desc populated for complex structures +import cbor2 +from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class FullRecord(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER('seq', 0), + CBORF_TEXT_STRING('source', ''), + CBORF_FLOAT('score', 0.0), + CBORF_BYTE_STRING('checksum', b''), + ) + +field_names = [f.name for f in FullRecord.fields_desc] +'seq' in field_names and 'source' in field_names and 'score' in field_names and 'checksum' in field_names + += Realistic model: multi-field packet encoding is byte-for-byte reproducible +import cbor2 +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_FLOAT +from scapy.cborpacket import CBOR_Packet + +class MeasurementPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER('seq', 0), + CBORF_TEXT_STRING('sensor', ''), + CBORF_FLOAT('value', 0.0), + CBORF_BYTE_STRING('raw', b''), + ) + +pkt = MeasurementPkt() +pkt.seq.val = 42 +pkt.sensor.val = 'temp-01' +pkt.value.val = 23.5 +pkt.raw.val = b'\x01\x02' +raw1 = bytes(pkt) +raw2 = bytes(MeasurementPkt(raw1)) +raw1 == raw2 + +########### CBOR Fuzzing / Random Object Tests #################### + ++ CBOR Random Object Generation + += Create RandCBORObject +from scapy.cbor import RandCBORObject +rand = RandCBORObject() +isinstance(rand, RandCBORObject) + += Generate random CBOR unsigned integer +from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER +rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) +obj = rand._fix() +isinstance(obj, CBOR_UNSIGNED_INTEGER) and isinstance(obj.val, int) and obj.val >= 0 + += Generate random CBOR negative integer +from scapy.cbor import RandCBORObject, CBOR_NEGATIVE_INTEGER +rand = RandCBORObject(objlist=[CBOR_NEGATIVE_INTEGER]) +obj = rand._fix() +isinstance(obj, CBOR_NEGATIVE_INTEGER) and isinstance(obj.val, int) and obj.val < 0 + += Generate random CBOR byte string +from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING +rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) +obj = rand._fix() +isinstance(obj, CBOR_BYTE_STRING) and isinstance(obj.val, bytes) + += Generate random CBOR text string +from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING +rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) +obj = rand._fix() +isinstance(obj, CBOR_TEXT_STRING) and isinstance(obj.val, str) and len(obj.val) > 0 + += Generate random CBOR array +from scapy.cbor import RandCBORObject, CBOR_ARRAY +rand = RandCBORObject(objlist=[CBOR_ARRAY]) +obj = rand._fix() +isinstance(obj, CBOR_ARRAY) and isinstance(obj.val, list) + += Generate random CBOR map +from scapy.cbor import RandCBORObject, CBOR_MAP +rand = RandCBORObject(objlist=[CBOR_MAP]) +obj = rand._fix() +isinstance(obj, CBOR_MAP) and isinstance(obj.val, dict) + += Generate random CBOR boolean (false) +from scapy.cbor import RandCBORObject, CBOR_FALSE +rand = RandCBORObject(objlist=[CBOR_FALSE]) +obj = rand._fix() +isinstance(obj, CBOR_FALSE) and obj.val == False + += Generate random CBOR boolean (true) +from scapy.cbor import RandCBORObject, CBOR_TRUE +rand = RandCBORObject(objlist=[CBOR_TRUE]) +obj = rand._fix() +isinstance(obj, CBOR_TRUE) and obj.val == True + += Generate random CBOR null +from scapy.cbor import RandCBORObject, CBOR_NULL +rand = RandCBORObject(objlist=[CBOR_NULL]) +obj = rand._fix() +isinstance(obj, CBOR_NULL) and obj.val is None + += Generate random CBOR undefined +from scapy.cbor import RandCBORObject, CBOR_UNDEFINED +rand = RandCBORObject(objlist=[CBOR_UNDEFINED]) +obj = rand._fix() +isinstance(obj, CBOR_UNDEFINED) and obj.val is None + += Generate random CBOR float +from scapy.cbor import RandCBORObject, CBOR_FLOAT +rand = RandCBORObject(objlist=[CBOR_FLOAT]) +obj = rand._fix() +isinstance(obj, CBOR_FLOAT) and isinstance(obj.val, float) + ++ CBOR Random Object Encoding/Decoding + += Encode and decode random unsigned integer +from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_UNSIGNED_INTEGER) and remainder == b'' and decoded.val == obj.val + += Encode and decode random text string +from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_TEXT_STRING) and remainder == b'' and decoded.val == obj.val + += Encode and decode random byte string +from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_BYTE_STRING) and remainder == b'' and decoded.val == obj.val + += Encode and decode random array +from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_ARRAY]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_ARRAY) and remainder == b'' and len(decoded.val) == len(obj.val) + += Encode and decode random map +from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_MAP]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_MAP) and remainder == b'' and len(decoded.val) == len(obj.val) + += Encode and decode random float +from scapy.cbor import RandCBORObject, CBOR_FLOAT, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_FLOAT]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_FLOAT) and remainder == b'' + ++ CBOR Random Mixed Types + += Generate multiple random objects of different types +from scapy.cbor import RandCBORObject +rand = RandCBORObject() +objects = [rand._fix() for _ in range(10)] +len(objects) == 10 and all(hasattr(obj, 'val') for obj in objects) + += Encode and decode multiple random objects +from scapy.cbor import RandCBORObject, CBOR_Codecs +rand = RandCBORObject() +success_count = 0 +for _ in range(20): + obj = rand._fix() + try: + encoded = bytes(obj) + decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) + if remainder == b'': + success_count += 1 + except: + pass + +success_count >= 18 + += Random nested arrays encode/decode correctly +from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_ARRAY]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_ARRAY) and remainder == b'' + += Random nested maps encode/decode correctly +from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs +rand = RandCBORObject(objlist=[CBOR_MAP]) +obj = rand._fix() +encoded = bytes(obj) +decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) +isinstance(decoded, CBOR_MAP) and remainder == b'' + ++ CBOR Fuzzing Stress Tests + += Generate 100 random objects without errors +from scapy.cbor import RandCBORObject +rand = RandCBORObject() +objects = [] +for _ in range(100): + obj = None + try: + obj = rand._fix() + except: + pass + if obj is not None: + objects.append(obj) + +len(objects) >= 95 + += Encode 50 random objects without errors +from scapy.cbor import RandCBORObject +rand = RandCBORObject() +encoded_count = 0 +for _ in range(50): + obj = rand._fix() + try: + encoded = bytes(obj) + if len(encoded) > 0: + encoded_count += 1 + except: + pass + +encoded_count >= 45 + += Roundtrip 50 random objects +from scapy.cbor import RandCBORObject, CBOR_Codecs +rand = RandCBORObject() +roundtrip_count = 0 +for _ in range(50): + obj = rand._fix() + try: + encoded = bytes(obj) + decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) + if remainder == b'': + roundtrip_count += 1 + except: + pass + +roundtrip_count >= 45 diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index b8b1202f8bf..add646d3338 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -212,3 +212,32 @@ for opt_class in EDNS0OPT_DISPATCHER.values(): p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0TLV(), opt_class(), opt_class()]))) assert len(p.rdata) == 3 assert all(Raw not in opt for opt in p.rdata) + + ++ EDNS0 - Owner + += Dissection + +p = EDNS0OWN(b'\x00\x04\x00\x08\x00\x9b\x11"3DUf') +assert p.optcode == 4 +assert p.optlen == 8 +assert p.v == 0 +assert p.s == 155 +assert p.primary_mac == '11:22:33:44:55:66' + +p = EDNS0OWN(b'\x00\x04\x00\x0e\x00\x9b\x11"3DUffUD3"\x11') +assert p.optcode == 4 +assert p.optlen == 14 +assert p.v == 0 +assert p.s == 155 +assert p.primary_mac == '11:22:33:44:55:66' +assert p.wakeup_mac == '66:55:44:33:22:11' + +p = EDNS0OWN(b'\x00\x04\x00\x12\x00\x9b\x11"3DUffUD3"\x11abcd') +assert p.optcode == 4 +assert p.optlen == 18 +assert p.v == 0 +assert p.s == 155 +assert p.primary_mac == '11:22:33:44:55:66' +assert p.wakeup_mac == '66:55:44:33:22:11' +assert p.password == b'abcd' diff --git a/test/scapy/layers/dot15d4.uts b/test/scapy/layers/dot15d4.uts index 3562a8e579e..15ad74de801 100644 --- a/test/scapy/layers/dot15d4.uts +++ b/test/scapy/layers/dot15d4.uts @@ -325,6 +325,80 @@ p = Dot15d4AuxSecurityHeader(b"\x18\x05\x00\x00\x00\xff\xee\xdd\xcc\xbb\xaa\x00\ assert p.sec_sc_keyidmode == 3 assert p.sec_keyid_keysource == 11024999611375677183 += Dot15d4AuxSecurityHeader - extract_padding does not consume trailing bytes + +p = Dot15d4AuxSecurityHeader(b"\x04\x05\x00\x00\x00\xAA\xBB") +assert p.sec_sc_seclevel == 4 +assert p.sec_framecounter == 0x5 +assert Raw not in p +assert Padding in p +assert p[Padding].load == b"\xAA\xBB" + += Dot15d4 Beacon with aux_sec_header (issue #4928) + +# Given: raw bytes for a Dot15d4 Beacon frame with fcf_security=1 +# Note: remaining bytes are encrypted data, so ZigBeeBeacon dissection is expected to fail +with no_debug_dissector(): + pkt = Dot15d4(b'\x08\xD0\x84\x21\x43\x01\x00\x00\x00\x00\x48\xDE\xAC\x02\x05\x00\x00\x00\x55\xCF\x00\x00\x51\x52\x53\x54\x22\x3B\xC1\xEC\x84\x1A\xB5\x53') + +assert pkt.fcf_frametype == 0 +assert pkt.fcf_security == 1 +assert Dot15d4Beacon in pkt +assert pkt[Dot15d4Beacon].aux_sec_header is not None +assert pkt[Dot15d4Beacon].aux_sec_header.sec_sc_seclevel == 2 +assert pkt[Dot15d4Beacon].aux_sec_header.sec_sc_keyidmode == 0 +assert pkt[Dot15d4Beacon].aux_sec_header.sec_framecounter == 0x5 +assert Raw in pkt + += Dot15d4 Data with aux_sec_header - build & dissect round-trip + +# Given: a Dot15d4 Data frame with fcf_security=1 +pkt = Dot15d4(fcf_frametype=1, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Data(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=5, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01)) +# When: packet is serialized and re-dissected +pkt2 = Dot15d4(raw(pkt)) +# Then: aux_sec_header is correctly parsed +assert pkt2.fcf_security == 1 +assert Dot15d4Data in pkt2 +assert pkt2[Dot15d4Data].aux_sec_header is not None +assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_seclevel == 5 +assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_keyidmode == 1 +assert pkt2[Dot15d4Data].aux_sec_header.sec_keyid_keyindex == 0x01 + += Dot15d4 Cmd with aux_sec_header - build & dissect round-trip + +# Given: a Dot15d4 Command frame with fcf_security=1 +pkt = Dot15d4(fcf_frametype=3, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Cmd(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=5, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01), cmd_id=4) +# When: packet is serialized and re-dissected +pkt2 = Dot15d4(raw(pkt)) +# Then: aux_sec_header is correctly parsed +assert pkt2.fcf_security == 1 +assert Dot15d4Cmd in pkt2 +assert pkt2[Dot15d4Cmd].aux_sec_header is not None +assert pkt2[Dot15d4Cmd].aux_sec_header.sec_sc_seclevel == 5 +assert pkt2[Dot15d4Cmd].aux_sec_header.sec_sc_keyidmode == 1 + += Dot15d4 Data with encrypted payload (seclevel >= 4) stays Raw + +# Given: a Data frame with seclevel=4 (ENC) and trailing encrypted bytes +pkt = Dot15d4(fcf_frametype=1, fcf_security=1, fcf_destaddrmode=2, fcf_srcaddrmode=2) / Dot15d4Data(dest_panid=0x1234, dest_addr=0xFFFF, src_panid=0x1234, src_addr=0x0001, aux_sec_header=Dot15d4AuxSecurityHeader(sec_sc_seclevel=4, sec_sc_keyidmode=1, sec_keyid_keyindex=0x01)) / Raw(b'\xaa\xbb\xcc\xdd') +# When: packet is serialized and re-dissected +pkt2 = Dot15d4(raw(pkt)) +# Then: encrypted payload must not be dissected as SixLoWPAN/ZigBee +assert pkt2[Dot15d4Data].aux_sec_header.sec_sc_seclevel == 4 +assert Raw in pkt2 +from scapy.layers.sixlowpan import SixLoWPAN +assert SixLoWPAN not in pkt2 + += Dot15d4 Beacon without aux_sec_header (fcf_security=0) + +# Given: raw bytes for a Dot15d4 Beacon frame with fcf_security=0 +pkt = Dot15d4FCS(b'\x00\x80\x89\xaa\x99\x00\x00\xff\xcf\x00\x00\x00"\x84\xfe\xca\xef\xbe\xed\xfe\xce\xfa\xff\xff\xff\x00X\xa4') +# When: packet is dissected +# Then: fcf_security is not set and aux_sec_header is None +assert pkt.fcf_security == 0 +assert Dot15d4Beacon in pkt +assert pkt[Dot15d4Beacon].aux_sec_header is None + # RPL: unimplemented #p = SixLoWPAN(b"\x7b\x3b\x3a\x1a\x9b\x02\xae\x30\x21\x00\x00\xef\x05\x12\x00\x80\x20\x02\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x33\x44\x09\x04\x00\x00\x00\x00\x06\x04\x00\x01\xef\xff") #p.show2() diff --git a/test/scapy/layers/hsrp.uts b/test/scapy/layers/hsrp.uts index eeabeb0fea3..00bbbb7340d 100644 --- a/test/scapy/layers/hsrp.uts +++ b/test/scapy/layers/hsrp.uts @@ -12,4 +12,23 @@ assert pkt[IP].dst == "224.0.0.2" and pkt[UDP].sport == pkt[UDP].dport == 1985 assert pkt[HSRP].opcode == 0 and pkt[HSRP].state == 16 assert pkt[HSRPmd5].type == 4 and pkt[HSRPmd5].sourceip == defaddr - += HSRP - Advertise build & dissection +advertise_raw = b"\x00\x03\x00\x01\x00\x0e\x02\x00\x00\x00\x00\x01o\x00\x00\x00" +pkt = HSRP(advertise_raw) +assert isinstance(pkt, HSRPAdvertise) +assert pkt.opcode == 3 +assert pkt.type == 1 and pkt.length == 14 +assert pkt.state == 2 and pkt.reserved1 == 0 +assert pkt.activegroups == 0 and pkt.passivegroups == 1 +assert pkt.reserved2 == 0x6f000000 +assert raw( + HSRPAdvertise( + type=1, + length=14, + state=2, + reserved1=0, + activegroups=0, + passivegroups=1, + reserved2=0x6f000000, + ) +) == advertise_raw diff --git a/test/scapy/layers/http.uts b/test/scapy/layers/http.uts index 541200da006..e090bf8b243 100644 --- a/test/scapy/layers/http.uts +++ b/test/scapy/layers/http.uts @@ -309,6 +309,7 @@ class run_httpserver: iface=conf.loopback_name, mech=self.mech, ssp=self.ssp, bg=True, + debug=4, **self.kwargs, ) # wait for it to start diff --git a/test/scapy/layers/ldap.uts b/test/scapy/layers/ldap.uts index a4d1892e909..7657daea674 100644 --- a/test/scapy/layers/ldap.uts +++ b/test/scapy/layers/ldap.uts @@ -215,3 +215,23 @@ pkt = NETLOGON(b'\x13\x00\\\x00\\\x00D\x00C\x001\x00\x00\x00\x00\x00D\x00O\x00M\ assert pkt.NtVersion == 1 assert pkt.UnicodeLogonServer == r"\\DC1" assert pkt.UnicodeDomainName == "DOMAIN" + += Dissect NETLOGON_LOGON_QUERY - V1+V5+V5EX_WITH_IP + +pkt = NETLOGON(b'\x07\x00PC\x00\\MAILSLOT\\NET\\GETDC598\x00P\x00C\x00\x00\x00\x0b\x00\x00 \xff\xff\xff\xff') + +print(pkt.show()) +assert pkt.ComputerName == b"PC" +assert pkt.MailslotName == b"\\MAILSLOT\\NET\\GETDC598" +assert pkt.NtVersion == 0x2000000b +assert pkt.UnicodeComputerName == "PC" + += Dissect NETLOGON_LOGON_QUERY - V1+V5+V5EX_WITH_IP - with Padding + +pkt = NETLOGON(b'\x07\x00USER-PC\x00\\MAILSLOT\\NET\\GETDC725\x00\x00U\x00S\x00E\x00R\x00-\x00P\x00C\x00\x00\x00\x0b\x00\x00 \xff\xff\xff\xff') + +print(pkt.show()) +assert pkt.ComputerName == b"USER-PC" +assert pkt.MailslotName == b"\\MAILSLOT\\NET\\GETDC725" +assert pkt.NtVersion == 0x2000000b +assert pkt.UnicodeComputerName == "USER-PC" diff --git a/test/scapy/layers/netbios.uts b/test/scapy/layers/netbios.uts index eaff95decfe..9145b328284 100644 --- a/test/scapy/layers/netbios.uts +++ b/test/scapy/layers/netbios.uts @@ -8,7 +8,7 @@ = NBNSQueryRequest - build & dissect -z = NBNSHeader()/NBNSQueryRequest(SUFFIX="file server service", QUESTION_NAME='TEST1', QUESTION_TYPE='NB') +z = NBNSHeader()/NBNSQueryRequest(SUFFIX="File Server Service", QUESTION_NAME='TEST1', QUESTION_TYPE='NB') assert raw(z) == b'\x00\x00\x01\x10\x00\x01\x00\x00\x00\x00\x00\x00 FEEFFDFEDBCACACACACACACACACACACA\x00\x00 \x00\x01' diff --git a/test/scapy/layers/ntlm.uts b/test/scapy/layers/ntlm.uts index 8bc38dceb66..6b7f9766a72 100644 --- a/test/scapy/layers/ntlm.uts +++ b/test/scapy/layers/ntlm.uts @@ -186,8 +186,8 @@ assert ntlm_nego.NegotiateFlags.NEGOTIATE_UNICODE and ntlm_nego.NegotiateFlags.N assert ntlm_nego.NegotiateFlags == 0xe2898235 assert ntlm_nego.ProductMajorVersion == 10 assert ntlm_nego.ProductMinorVersion == 0 -assert ntlm_nego.ProductBuild == 19041 -assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00aJ\x00\x00\x00\x0f' +assert ntlm_nego.ProductBuild == 26100 +assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\xf4e\x00\x00\x00\x0f' = GSS_Accept_sec_context (SPNEGO_negTokenResp: NTLM_NEGOTIATE->NTLM_CHALLENGE) diff --git a/test/scapy/layers/smbclientserver.uts b/test/scapy/layers/smbclientserver.uts index 101843cbdc6..b770a5f4424 100644 --- a/test/scapy/layers/smbclientserver.uts +++ b/test/scapy/layers/smbclientserver.uts @@ -480,3 +480,19 @@ with run_smbserver(readonly=False, encryptshare=True): raise finally: cli.close() + + ++ Windows-only SMB tests +~ windows + += smbclient: use WinSSP to connect to the loopback + +from scapy.arch.windows.sspi import WinSSP + +try: + cli = smbclient("127.0.0.1", ssp=WinSSP(), cli=False) + results = cli.shares() + print(results) + assert any(x[0] == "IPC$" for x in results) +finally: + cli.close() diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index 35a8d050059..cb3b363397e 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -329,7 +329,13 @@ x.notAfter == (2026, 3, 30, 7, 38, 59, 0, 89, -1) = Cert class : test remainingDays assert abs(x.remainingDays("02/12/11")) > 5000 -assert abs(x.remainingDays("Feb 12 10:00:00 2011 Paris, Madrid")) > 1 +assert abs(x.remainingDays("Feb 12 10:00:00 2011 UTC")) > 5000 + +from unittest.mock import patch +import time + +with patch('time.localtime', return_value=time.struct_time((2026, 3, 1, 0, 0, 0, 6, 60, 0))): + assert abs(x.remainingDays("Feb 12 10:00:00 2011 Paris, Madrid")) > 1 = Cert class : Checking RSA public key assert type(x.pubkey) is PubKeyRSA @@ -1104,4 +1110,4 @@ assert [x.buffer for x in aikOpaque.policyDigestList.digests] == [ b'\xc4\x13\xa8G\xb1\x11\x12\xb1\xcb\xdd\xd4\xec\xa4\xda\xaa\x15\xa1\x85,\x1c;\xbaWF\x1d%v\x05\xf3\xd5\xafS', b'', b'\x04\x8e\x9a:\xce\x08X?y\xf3D\xffx[\xbe\xa9\xf0z\xc7\xfa3%\xb3\xd4\x9a!\xddQ\x94\xc6XP', -] \ No newline at end of file +] diff --git a/test/scapy/layers/tls/tls.uts b/test/scapy/layers/tls/tls.uts index 0fff89f2f37..3c3d47b834d 100644 --- a/test/scapy/layers/tls/tls.uts +++ b/test/scapy/layers/tls/tls.uts @@ -17,35 +17,35 @@ = Crypto - Hmac_MD5 instantiation, parameter check from scapy.layers.tls.crypto.h_mac import Hmac_MD5 -a = Hmac_MD5("somekey") +a = Hmac_MD5(b"somekey") a.key_len == 16 and a.hmac_len == 16 = Crypto - Hmac_MD5 behavior on test vectors from RFC 2202 (+ errata) a = Hmac_MD5 -t1 = a(b'\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b').digest("Hi There") == b'\x92\x94\x72\x7a\x36\x38\xbb\x1c\x13\xf4\x8e\xf8\x15\x8b\xfc\x9d' -t2 = a('Jefe').digest('what do ya want for nothing?') == b'\x75\x0c\x78\x3e\x6a\xb0\xb5\x03\xea\xa8\x6e\x31\x0a\x5d\xb7\x38' +t1 = a(b'\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b').digest(b"Hi There") == b'\x92\x94\x72\x7a\x36\x38\xbb\x1c\x13\xf4\x8e\xf8\x15\x8b\xfc\x9d' +t2 = a(b'Jefe').digest(b'what do ya want for nothing?') == b'\x75\x0c\x78\x3e\x6a\xb0\xb5\x03\xea\xa8\x6e\x31\x0a\x5d\xb7\x38' t3 = a(b'\xaa'*16).digest(b'\xdd'*50) == b'\x56\xbe\x34\x52\x1d\x14\x4c\x88\xdb\xb8\xc7\x33\xf0\xe8\xb3\xf6' t4 = a(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19').digest(b'\xcd'*50) == b'\x69\x7e\xaf\x0a\xca\x3a\x3a\xea\x3a\x75\x16\x47\x46\xff\xaa\x79' -t5 = a(b'\x0c'*16).digest("Test With Truncation") == b'\x56\x46\x1e\xf2\x34\x2e\xdc\x00\xf9\xba\xb9\x95\x69\x0e\xfd\x4c' -t6 = a(b'\xaa'*80).digest("Test Using Larger Than Block-Size Key - Hash Key First") == b'\x6b\x1a\xb7\xfe\x4b\xd7\xbf\x8f\x0b\x62\xe6\xce\x61\xb9\xd0\xcd' -t7 = a(b'\xaa'*80).digest("Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data") == b'\x6f\x63\x0f\xad\x67\xcd\xa0\xee\x1f\xb1\xf5\x62\xdb\x3a\xa5\x3e' +t5 = a(b'\x0c'*16).digest(b"Test With Truncation") == b'\x56\x46\x1e\xf2\x34\x2e\xdc\x00\xf9\xba\xb9\x95\x69\x0e\xfd\x4c' +t6 = a(b'\xaa'*80).digest(b"Test Using Larger Than Block-Size Key - Hash Key First") == b'\x6b\x1a\xb7\xfe\x4b\xd7\xbf\x8f\x0b\x62\xe6\xce\x61\xb9\xd0\xcd' +t7 = a(b'\xaa'*80).digest(b"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data") == b'\x6f\x63\x0f\xad\x67\xcd\xa0\xee\x1f\xb1\xf5\x62\xdb\x3a\xa5\x3e' t1 and t2 and t3 and t4 and t5 and t6 and t7 = Crypto - Hmac_SHA instantiation, parameter check from scapy.layers.tls.crypto.h_mac import Hmac_SHA -a = Hmac_SHA("somekey") +a = Hmac_SHA(b"somekey") a.key_len == 20 and a.hmac_len == 20 = Crypto - Hmac_SHA behavior on test vectors from RFC 2202 (+ errata) a = Hmac_SHA -t1 = a(b'\x0b'*20).digest("Hi There") == b'\xb6\x17\x31\x86\x55\x05\x72\x64\xe2\x8b\xc0\xb6\xfb\x37\x8c\x8e\xf1\x46\xbe\x00' -t2 = a('Jefe').digest("what do ya want for nothing?") == b'\xef\xfc\xdf\x6a\xe5\xeb\x2f\xa2\xd2\x74\x16\xd5\xf1\x84\xdf\x9c\x25\x9a\x7c\x79' +t1 = a(b'\x0b'*20).digest(b"Hi There") == b'\xb6\x17\x31\x86\x55\x05\x72\x64\xe2\x8b\xc0\xb6\xfb\x37\x8c\x8e\xf1\x46\xbe\x00' +t2 = a(b'Jefe').digest(b"what do ya want for nothing?") == b'\xef\xfc\xdf\x6a\xe5\xeb\x2f\xa2\xd2\x74\x16\xd5\xf1\x84\xdf\x9c\x25\x9a\x7c\x79' t3 = a(b'\xaa'*20).digest(b'\xdd'*50) == b'\x12\x5d\x73\x42\xb9\xac\x11\xcd\x91\xa3\x9a\xf4\x8a\xa1\x7b\x4f\x63\xf1\x75\xd3' t4 = a(b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19').digest(b'\xcd'*50) == b'\x4c\x90\x07\xf4\x02\x62\x50\xc6\xbc\x84\x14\xf9\xbf\x50\xc8\x6c\x2d\x72\x35\xda' -t5 = a(b'\x0c'*20).digest("Test With Truncation") == b'\x4c\x1a\x03\x42\x4b\x55\xe0\x7f\xe7\xf2\x7b\xe1\xd5\x8b\xb9\x32\x4a\x9a\x5a\x04' -t6 = a(b'\xaa'*80).digest("Test Using Larger Than Block-Size Key - Hash Key First") == b'\xaa\x4a\xe5\xe1\x52\x72\xd0\x0e\x95\x70\x56\x37\xce\x8a\x3b\x55\xed\x40\x21\x12' -t7 = a(b'\xaa'*80).digest("Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data") == b'\xe8\xe9\x9d\x0f\x45\x23\x7d\x78\x6d\x6b\xba\xa7\x96\x5c\x78\x08\xbb\xff\x1a\x91' +t5 = a(b'\x0c'*20).digest(b"Test With Truncation") == b'\x4c\x1a\x03\x42\x4b\x55\xe0\x7f\xe7\xf2\x7b\xe1\xd5\x8b\xb9\x32\x4a\x9a\x5a\x04' +t6 = a(b'\xaa'*80).digest(b"Test Using Larger Than Block-Size Key - Hash Key First") == b'\xaa\x4a\xe5\xe1\x52\x72\xd0\x0e\x95\x70\x56\x37\xce\x8a\x3b\x55\xed\x40\x21\x12' +t7 = a(b'\xaa'*80).digest(b"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data") == b'\xe8\xe9\x9d\x0f\x45\x23\x7d\x78\x6d\x6b\xba\xa7\x96\x5c\x78\x08\xbb\xff\x1a\x91' t1 and t2 and t3 and t4 and t5 and t6 and t7 @@ -304,21 +304,21 @@ t1 and t2 and t3 and t4 and t5 and t6 and t7 from scapy.layers.tls.crypto.prf import PRF class _prf_tls12_sha256_test: - h= "SHA256" + h= "sha256" k= b"\x9b\xbe\x43\x6b\xa9\x40\xf0\x17\xb1\x76\x52\x84\x9a\x71\xdb\x35" s= b"\xa0\xba\x9f\x93\x6c\xda\x31\x18\x27\xa6\xf7\x96\xff\xd5\x19\x8c" o=(b"\xe3\xf2\x29\xba\x72\x7b\xe1\x7b\x8d\x12\x26\x20\x55\x7c\xd4\x53" + b"\xc2\xaa\xb2\x1d\x07\xc3\xd4\x95\x32\x9b\x52\xd4\xe6\x1e\xdb\x5a") class _prf_tls12_sha384_test: - h= "SHA384" + h= "sha384" k= b"\xb8\x0b\x73\x3d\x6c\xee\xfc\xdc\x71\x56\x6e\xa4\x8e\x55\x67\xdf" s= b"\xcd\x66\x5c\xf6\xa8\x44\x7d\xd6\xff\x8b\x27\x55\x5e\xdb\x74\x65" o=(b"\x7b\x0c\x18\xe9\xce\xd4\x10\xed\x18\x04\xf2\xcf\xa3\x4a\x33\x6a" + b"\x1c\x14\xdf\xfb\x49\x00\xbb\x5f\xd7\x94\x21\x07\xe8\x1c\x83\xcd") class _prf_tls12_sha512_test: - h= "SHA512" + h= "sha512" k= b"\xb0\x32\x35\x23\xc1\x85\x35\x99\x58\x4d\x88\x56\x8b\xbb\x05\xeb" s= b"\xd4\x64\x0e\x12\xe4\xbc\xdb\xfb\x43\x7f\x03\xe6\xae\x41\x8e\xe5" o=(b"\x12\x61\xf5\x88\xc7\x98\xc5\xc2\x01\xff\x03\x6e\x7a\x9c\xb5\xed" + @@ -1028,6 +1028,7 @@ assert len(pkt.exchkeys.ecdh_Yc) == 133 # len(b'\x04') + ceil(521/8) * 2 # See https://github.com/secdev/scapy/issues/2784 +import base64 from scapy.layers.tls.cert import PrivKey from scapy.layers.tls.handshake import TLSFinished from scapy.layers.tls.record import TLS @@ -1067,7 +1068,7 @@ r2 = TLS(shello_extms, tls_session=r1.tls_session.mirror()) r3 = TLS(finished_extms, tls_session=r2.tls_session.mirror()) assert r3.tls_session.extms -assert r3.tls_session.pwcs.prf.hash_name == "SHA256" +assert r3.tls_session.pwcs.prf.hash_name == "sha256" assert r3.tls_session.session_hash == b'2\xdc\xf5\xcb\xbc\x99\xc6IV\xba\x0f.\x0bdq\x1f=\xef\xdaW\xfc*A\x9b\xe2?b\xccKW\xe9\xb7' l3 = r3.getlayer(TLS, 3) diff --git a/test/testsocket.py b/test/testsocket.py index 1ecd79f4fec..e6577b15fb0 100644 --- a/test/testsocket.py +++ b/test/testsocket.py @@ -183,7 +183,8 @@ class SlowTestSocket(TestSocket): PythonCANSocket on a slow serial interface (like slcan). Frames sent to this socket go into an intermediate serial buffer. - They only become visible to recv()/select() after mux() moves + They only become visible to recv()/select() after _mux() moves + them to the rx ObjectPipe. Key parameters model the real slcan timing bottleneck: @@ -221,6 +222,7 @@ def __init__(self, basecls=None, frame_delay=0.0002, self.interface_name = interface_name from collections import deque self._serial_buffer = deque() # type: deque[bytes] + self._serial_lock = Lock() self._last_mux = 0.0 self._frame_delay = frame_delay @@ -258,6 +260,7 @@ def _mux(self): return # Phase 1: read_bus — read frames from serial buffer + msgs = [] deadline = time.monotonic() + self._read_time_limit \ if self._read_time_limit > 0 else None @@ -281,6 +284,7 @@ def _mux(self): break # Phase 2: distribute — apply per-socket filtering + for frame in msgs: if self._can_filters is not None: can_id = self._extract_can_id(frame) @@ -292,7 +296,8 @@ def _mux(self): def recv_raw(self, x=MTU): # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 - """Read from the rx ObjectPipe (populated by mux via select).""" + """Read from the rx ObjectPipe (populated by _mux via select).""" + return self.basecls, self._real_ins.recv(0), time.time() def send(self, x): @@ -344,6 +349,114 @@ def closed(self): return bool(self._owner._real_ins.closed) # type: ignore[attr-defined] +class USBTestSocket(TestSocket): + """A TestSocket that simulates the hardware RX FIFO of USB CAN + adapters like candle (gs_usb) and cantact. + + USB adapters have a small hardware endpoint buffer (typically + 32-128 frames). Frames that arrive when the buffer is full + are silently dropped by the adapter firmware. + + Frames sent to this socket go into a capacity-limited deque + (the "hardware FIFO"). They only become visible to recv()/select() + after _mux() moves them to the rx ObjectPipe — which happens when + ISOTPSocketImplementation calls can_socket.select() in its + can_recv callback, or in the drain calls in __init__/close. + + When nobody calls select/recv (e.g., between ISOTP socket close + and reopen), frames accumulate in the FIFO. Once the FIFO is + full, new frames are silently dropped, simulating hardware + overflow. + """ + + def __init__(self, basecls=None, hw_fifo_size=32): + # type: (Optional[Type[Packet]], int) -> None + """ + :param hw_fifo_size: Maximum number of frames in the simulated + hardware RX FIFO. Default 32 models a typical USB endpoint + buffer. Frames beyond this limit are silently dropped. + """ + super(USBTestSocket, self).__init__(basecls) + from collections import deque + self._hw_fifo = deque(maxlen=hw_fifo_size) # type: deque[bytes] + self._hw_lock = Lock() + self._real_ins = self.ins + self.ins = _USBPipeWrapper(self) # type: ignore[assignment] + self.dropped_count = 0 + + def _mux(self): + # type: () -> None + """Move frames from hardware FIFO to the rx ObjectPipe. + + This models the read path of PythonCANSocket.select() → + multiplex_rx_packets() → read_bus(). On real hardware this + is the USB bulk transfer that moves frames from the adapter + endpoint buffer into the host-side python-can rx_queue. + """ + with self._hw_lock: + while self._hw_fifo: + frame = self._hw_fifo.popleft() + self._real_ins.send(frame) + + def recv_raw(self, x=MTU): + # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 + return self.basecls, self._real_ins.recv(0), time.time() + + @staticmethod + def select(sockets, remain=conf.recv_poll_rate): + # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + for s in sockets: + if isinstance(s, USBTestSocket): + s._mux() + return select_objects(sockets, remain) + + def close(self): + # type: () -> None + self.ins = self._real_ins + super(USBTestSocket, self).close() + + +class _USBPipeWrapper: + """Wrapper that routes incoming frames into the hardware FIFO. + + When the FIFO is full (maxlen reached), deque silently drops the + oldest frame — but real USB adapters drop the *newest* frame. + We track drops via owner.dropped_count so the test can verify + overflow occurred. + """ + def __init__(self, owner): + # type: (USBTestSocket) -> None + self._owner = owner + + def send(self, data): + # type: (bytes) -> None + with self._owner._hw_lock: + was_full = len(self._owner._hw_fifo) >= \ + (self._owner._hw_fifo.maxlen or 0) + if was_full: + # Drop the incoming frame (newest) like real hardware + self._owner.dropped_count += 1 + return + self._owner._hw_fifo.append(data) + + def recv(self, timeout=0): + # type: (int) -> Optional[bytes] + return self._owner._real_ins.recv(timeout) + + def fileno(self): + # type: () -> int + return self._owner._real_ins.fileno() + + def close(self): + # type: () -> None + self._owner._real_ins.close() + + @property + def closed(self): + # type: () -> bool + return bool(self._owner._real_ins.closed) # type: ignore[attr-defined] + + def cleanup_testsockets(): # type: () -> None """ diff --git a/tox.ini b/tox.ini index 442ba9fdab1..42fe881b8ca 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,13 @@ # Tox environments: # py{version}-{os}-{non_root,root} -# In our testing, version can be 37 to 313 or py39 for pypy39 +# In our testing, version can be 37 to 314 or py311 for pypy311 [tox] # minversion = 4.0 skip_missing_interpreters = true # envlist = default when doing 'tox' -envlist = py{37,38,39,310,311,312,313}-{linux,bsd,windows}-{non_root,root} +envlist = py{37,38,39,310,311,312,313,314}-{linux,bsd,windows}-{non_root,root} # Main tests