diff --git a/poetry.lock b/poetry.lock index 516ad3d65..54ada3277 100644 --- a/poetry.lock +++ b/poetry.lock @@ -907,36 +907,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "psutil" -version = "6.1.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, - {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, - {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, - {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, - {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, - {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, - {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, - {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, -] - -[package.extras] -dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - [[package]] name = "pycparser" version = "2.22" @@ -1634,17 +1604,6 @@ files = [ {file = "types_aiofiles-24.1.0.20240626-py3-none-any.whl", hash = "sha256:7939eca4a8b4f9c6491b6e8ef160caee9a21d32e18534a57d5ed90aee47c66b4"}, ] -[[package]] -name = "types-psutil" -version = "6.1.0.20241102" -description = "Typing stubs for psutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-psutil-6.1.0.20241102.tar.gz", hash = "sha256:8cbe086b9c29f5c0aa55c4422498c07a8e506f096205761dba088905198551dc"}, - {file = "types_psutil-6.1.0.20241102-py3-none-any.whl", hash = "sha256:61f836f8ba48f28f0d290d3bcd902f9130ce5057a1676e6ecbefb6141e2743f4"}, -] - [[package]] name = "types-tabulate" version = "0.9.0.20240106" @@ -1886,4 +1845,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "ee3101bca1063b135590066e6d185e82ac31401dec0f4d3e57fc46b6dd485bcb" +content-hash = "f0edf20af83d5f45c9b2e7729254d771929081aac8e0dfee31a17d61e5ec78fe" diff --git a/pyproject.toml b/pyproject.toml index 71ab2aa1c..0a4858492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ construct = "^2.10" msgspec = ">=0.11,<0.19" pydantic = "^2.0" platformdirs = ">=2.6,<5.0" -psutil = ">=5.9.4,<7.0.0" more-itertools = "^10.3.0" pydantic-argparse = { path = "vendor/pydantic-argparse", develop = true } @@ -56,7 +55,6 @@ pytest = ">=7.1,<9.0" pytest-asyncio = ">=0.20,<0.25" python-lsp-server = "^1.5" types-aiofiles = ">=23.1,<25.0" -types-psutil = ">=5.9.5.10,<7.0.0.0" types-tabulate = "^0.9" myst-parser = ">=3.0.1,<4.1" sphinx-rtd-theme = ">=1,<3" diff --git a/src/gallia/commands/discover/doip.py b/src/gallia/commands/discover/doip.py index a1ebb0819..68d3e4128 100644 --- a/src/gallia/commands/discover/doip.py +++ b/src/gallia/commands/discover/doip.py @@ -9,7 +9,6 @@ from urllib.parse import parse_qs, urlparse import aiofiles -import psutil from gallia.command import AsyncScript from gallia.command.base import AsyncScriptConfig @@ -31,6 +30,7 @@ TimingAndCommunicationParameters, VehicleAnnouncementMessage, ) +from gallia.utils import AddrInfo, net_if_addrs logger = get_logger(__name__) @@ -490,33 +490,40 @@ async def read_diag_request_custom(self, conn: DoIPConnection) -> tuple[int | No continue return (None, payload.UserData) - async def run_udp_discovery(self) -> list[tuple[str, int]]: - all_ips = [] - found = [] + @staticmethod + def get_broadcast_addrs() -> list[AddrInfo]: + out = [] + for iface in net_if_addrs(): + if iface.is_up() or not iface.can_broadcast(): + continue - for iface in psutil.net_if_addrs().values(): - for ip in iface: - # we only work with broadcastable IPv4 - if ip.family != socket.AF_INET or ip.broadcast is None: + for addr in iface.addr_info: + # We only work with broadcastable IPv4. + if not addr.is_v4() or addr.broadcast is None: continue - all_ips.append(ip) + out.append(addr) + return out + + async def run_udp_discovery(self) -> list[tuple[str, int]]: + addrs = self.get_broadcast_addrs() + found = [] - for ip in all_ips: - logger.info(f"[💌] Sending DoIP VehicleIdentificationRequest to {ip.broadcast}") + for addr in addrs: + logger.info(f"[💌] Sending DoIP VehicleIdentificationRequest to {addr.broadcast}") sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setblocking(False) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.bind((ip.address, 0)) + sock.bind((addr.local, 0)) loop = asyncio.get_running_loop() hdr = GenericHeader(0xFF, PayloadTypes.VehicleIdentificationRequestMessage, 0x00) - await loop.sock_sendto(sock, hdr.pack(), (ip.broadcast, 13400)) + await loop.sock_sendto(sock, hdr.pack(), (addr.broadcast, 13400)) try: while True: - data, addr = await asyncio.wait_for(loop.sock_recvfrom(sock, 1024), 2) + data, from_addr = await asyncio.wait_for(loop.sock_recvfrom(sock, 1024), 2) info = VehicleAnnouncementMessage.unpack(data[8:]) logger.notice(f"[💝]: {addr} responded: {info}") - found.append(addr) + found.append(from_addr) except TimeoutError: logger.info("[💔] Reached timeout...") continue diff --git a/src/gallia/utils.py b/src/gallia/utils.py index 65b58efcb..0c078d577 100644 --- a/src/gallia/utils.py +++ b/src/gallia/utils.py @@ -8,8 +8,10 @@ import contextvars import importlib.util import ipaddress +import json import logging import re +import subprocess import sys from collections.abc import Awaitable, Callable from pathlib import Path @@ -18,6 +20,8 @@ from urllib.parse import urlparse import aiofiles +import pydantic +from pydantic.networks import IPvAnyAddress from gallia.log import Loglevel, get_logger @@ -26,6 +30,9 @@ from gallia.transports import TargetURI +logger = get_logger(__name__) + + def auto_int(arg: str) -> int: return int(arg, 0) @@ -276,29 +283,81 @@ def get_file_log_level(args: Any) -> Loglevel: CONTEXT_SHARED_VARIABLE = "logger_name" -ctxVar: contextvars.ContextVar[tuple[str, str | None]] = contextvars.ContextVar( +context: contextvars.ContextVar[tuple[str, str | None]] = contextvars.ContextVar( CONTEXT_SHARED_VARIABLE ) def set_task_handler_ctx_variable( - logger_name: str, task_name: str | None = None + logger_name: str, + task_name: str | None = None, ) -> contextvars.Context: ctx = contextvars.copy_context() - ctx.run(ctxVar.set, (logger_name, task_name)) + ctx.run(context.set, (logger_name, task_name)) return ctx def handle_task_error(fut: asyncio.Future[Any]) -> None: - (logger_name, task_name) = ctxVar.get((__name__, "Task")) + logger_name, task_name = context.get((__name__, "Task")) logger = get_logger(logger_name) if logger.name is __name__: - logger.warning( - f" {fut} did not have context variable '{CONTEXT_SHARED_VARIABLE}' set; please fix this for proper logging" - ) + logger.warning(f"BUG: {fut} had no context '{CONTEXT_SHARED_VARIABLE}' set") + logger.warning("BUG: please fix this to ensure the task name is logged correctly") try: fut.result() except BaseException as e: + task_name = task_name if task_name is not None else "Task" + # Info level is enough, since our aim is only to consume the stack trace - logger.info(f"{task_name if task_name is not None else 'Task'} ended with error: {e!r}") + logger.info(f"{task_name} ended with error: {e!r}") + + +class AddrInfo(pydantic.BaseModel): + family: str + local: IPvAnyAddress + prefixlen: int + broadcast: IPvAnyAddress | None = None + scope: str + label: str | None = None + valid_life_time: int + preferred_life_time: int + + def is_v4(self) -> bool: + return self.family == "inet" + + +class Interface(pydantic.BaseModel): + ifindex: int + ifname: str + flags: list[str] + mtu: int + qdisc: str + operstate: str + group: str + link_type: str + address: str | None = None + broadcast: str | None = None + addr_info: list[AddrInfo] + + def is_up(self) -> bool: + return self.operstate == "UP" + + def can_broadcast(self) -> bool: + return "BROADCAST" in self.flags + + +def net_if_addrs() -> list[Interface]: + if sys.platform != "linux": + raise NotImplementedError("net_if_addrs() is only supported on Linux platforms") + + p = subprocess.run(["ip", "-j", "address", "show"], capture_output=True, check=True) + + try: + return [Interface(**item) for item in json.loads(p.stdout.decode())] + except pydantic.ValidationError as e: + logger.error("BUG: A special case for `ip -j address show` is not handled!") + logger.error("Please report a bug including the following json string.") + logger.error("https://github.com/Fraunhofer-AISEC/gallia/issues") + logger.error(e.json()) + raise diff --git a/tests/pytest/test_net_if.py b/tests/pytest/test_net_if.py new file mode 100644 index 000000000..68113eefd --- /dev/null +++ b/tests/pytest/test_net_if.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: AISEC Pentesting Team +# +# SPDX-License-Identifier: Apache-2.0 + +from gallia.utils import net_if_addrs + + +def test_net_if_addrs() -> None: + net_if_addrs()