From 7cb2df1fc5195634c071472ae96876005fc5e7ef Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 13 May 2026 00:34:11 -0700 Subject: [PATCH] feat(net): LAN-only filter middleware (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-3 of the #433 web-exposed hardening track. Stacked on #473 (consumes ClientIdentity from #442). What ---- * New `pinky_daemon.net.lan_filter`: - `DEFAULT_PRIVATE_CIDRS` — RFC1918 + IPv4 link-local + IPv4 loopback + IPv6 ULA + IPv6 link-local + IPv6 loopback. CGNAT (100.64.0.0/10) is **not** included by design — operators on Tailscale opt in via `PINKY_LAN_EXTRA_CIDRS`. - `is_private_ip(addr, allowlist)` — pure helper with v4/v6 type guards so a mixed-family allowlist doesn't crash with TypeError. - `resolve_lan_allowed_cidrs(env)` — composes defaults + parsed extras from `PINKY_LAN_EXTRA_CIDRS` (lenient: malformed entries logged + skipped). - `build_lan_filter_middleware(allowlist)` — ASGI middleware factory: * No-op pass-through in `trusted` and `web` modes. * In `lan` mode, computes the canonical client IP via `resolve_client_identity` (so XFF parsing matches #442) and returns 403 when the IP isn't in the allowlist. * Logs reject at INFO with method, path, ip, raw_peer, via_proxy so scan/probe traffic is visible without flooding ERROR. * `create_api`: - Reads `PINKY_LAN_EXTRA_CIDRS` once, caches the composed allowlist on `app.state.lan_allowed_cidrs`. - Registers the LAN filter middleware **early** (before auth, before route handlers) so public-source traffic in LAN mode is dropped before any password-check work happens. Murzik review followups (from #436) ----------------------------------- * [x] Defer trusted-IP resolution to #442 — middleware uses `resolve_client_identity` end-to-end; no XFF parsing here. * [x] LAN middleware ordering: very early (registered first in create_api). * [x] Never trust XFF in LAN mode unless peer ∈ trusted_proxies — inherited from #442 via the resolver call. * [x] CIDR list: RFC1918 / link-local / ULA only. CGNAT explicitly excluded; opt-in via env. * [x] Test coverage: IPv6-mapped IPv4 (covered by #442 tests + is_private_ip type-guard test), comma-separated XFF (#442 tests), malformed XFF (#442 tests). Out of scope (deferred) ----------------------- * Bind defaults — landed in #472 (#434). * Refuse-to-boot when web + 0.0.0.0 without TLS marker — owned by #441. Tests (34 new) -------------- * `is_private_ip`: 9 private addresses accepted, 7 public/just-outside addresses rejected, CGNAT rejected by default + accepted with extras, mixed-family no-crash, empty allowlist rejects everything. * `resolve_lan_allowed_cidrs`: defaults when unset, extras appended, invalid extras skipped. * Middleware closure: trusted/web pass-through, LAN accepts private, LAN rejects public, LAN drops untrusted-peer XFF, LAN honors trusted-peer XFF, LAN honors operator-extra CIDRs, LAN falls back to env when no allowlist on state. * End-to-end through `create_api` (TestClient) for trusted (no filter), LAN (default-rejects testclient peer), LAN with extras (testclient peer allowed), web (no filter). Adjusted `test_deployment_mode::test_api_endpoint_exposes_mode_for_each_value` to opt 0.0.0.0/32 into the LAN allowlist so the loop reaches /api in every mode. Closes part of #433. Refs #436. 🤖 Authored by Barsik --- src/pinky_daemon/api.py | 15 ++ src/pinky_daemon/net/lan_filter.py | 189 ++++++++++++++++++ tests/test_deployment_mode.py | 13 +- tests/test_lan_filter.py | 304 +++++++++++++++++++++++++++++ 4 files changed, 517 insertions(+), 4 deletions(-) create mode 100644 src/pinky_daemon/net/lan_filter.py create mode 100644 tests/test_lan_filter.py diff --git a/src/pinky_daemon/api.py b/src/pinky_daemon/api.py index d6e89ba0..4f94390a 100644 --- a/src/pinky_daemon/api.py +++ b/src/pinky_daemon/api.py @@ -640,6 +640,10 @@ def create_api( from pinky_daemon.config import resolve_deployment_mode from pinky_daemon.net.client_identity import trusted_proxies_from_env + from pinky_daemon.net.lan_filter import ( + build_lan_filter_middleware, + resolve_lan_allowed_cidrs, + ) if deployment_mode is None: deployment_mode = resolve_deployment_mode() @@ -655,6 +659,17 @@ def create_api( # Trusted reverse-proxy CIDRs (#442). Read once at create_api time so the # canonical client-IP resolver doesn't hit os.environ on every request. app.state.trusted_proxies = trusted_proxies_from_env() + # LAN allowlist (#436): default RFC1918 + link-local + loopback + ULA, + # plus operator extras from PINKY_LAN_EXTRA_CIDRS. Cached so the + # middleware doesn't recompute per request. + app.state.lan_allowed_cidrs = resolve_lan_allowed_cidrs() + # LAN filter middleware (#436): no-op in trusted/web modes; in LAN mode + # rejects requests whose source isn't in app.state.lan_allowed_cidrs. + # Registered very early so public-source traffic is dropped before any + # auth/route handler work runs. + app.middleware("http")( + build_lan_filter_middleware(allowlist=app.state.lan_allowed_cidrs) + ) # ── CORS ────────────────────────────────────────────── # Allow origins from env (comma-separated) or default to same-origin only. diff --git a/src/pinky_daemon/net/lan_filter.py b/src/pinky_daemon/net/lan_filter.py new file mode 100644 index 00000000..1e38bcc4 --- /dev/null +++ b/src/pinky_daemon/net/lan_filter.py @@ -0,0 +1,189 @@ +"""LAN-only network filter (#436). + +Provides: + +* :data:`DEFAULT_PRIVATE_CIDRS` — the RFC1918 / link-local / loopback / ULA + ranges. CGNAT (``100.64.0.0/10``) is **not** included by default — operators + who run on Tailscale or another shared-CGNAT fabric have to opt in via the + ``PINKY_LAN_EXTRA_CIDRS`` env var so we don't accidentally trust the wider + carrier-grade NAT space. + +* :func:`is_private_ip` — pure helper that asks "is this address inside any of + these CIDRs?", with IPv4/IPv6 type guards so a misconfigured operator doesn't + blow up the request handler with a ``TypeError``. + +* :func:`build_lan_filter_middleware` — factory that returns an ASGI middleware + closure. Refuses any non-loopback / non-private source with HTTP 403 when + ``app.state.deployment_mode`` is :class:`DeploymentMode.LAN`. In ``trusted`` + and ``web`` modes the middleware is a no-op pass-through; the operator + declares intent through ``PINKY_DEPLOYMENT_MODE``, the middleware enforces + it. + +The middleware delegates IP resolution to +:func:`pinky_daemon.net.client_identity.resolve_client_identity` so XFF / +Forwarded handling, trust evaluation, and IPv6-mapped IPv4 normalisation +match the rest of the hardening track. + +Part of #433. Issue #436. +""" + +from __future__ import annotations + +import ipaddress +import logging +import os +from collections.abc import Awaitable, Callable + +from fastapi import Request +from fastapi.responses import JSONResponse + +from pinky_daemon.config import DeploymentMode +from pinky_daemon.net.client_identity import ( + IPAddress, + IPNetwork, + parse_trusted_proxies, + resolve_client_identity, +) + +__all__ = [ + "DEFAULT_PRIVATE_CIDRS", + "EXTRA_CIDRS_ENV", + "build_lan_filter_middleware", + "is_private_ip", + "resolve_lan_allowed_cidrs", +] + +logger = logging.getLogger(__name__) + +#: Env var listing operator-supplied additional CIDRs that should be treated +#: as "private" for LAN-mode filtering. Useful for Tailscale (``100.64.0.0/10``) +#: or VPN tenant networks. Comma-separated, same syntax as +#: ``PINKY_TRUSTED_PROXIES``. +EXTRA_CIDRS_ENV = "PINKY_LAN_EXTRA_CIDRS" + + +#: Default LAN allowlist — RFC1918 + IPv4 link-local + IPv4 loopback + IPv6 +#: ULA + IPv6 link-local + IPv6 loopback. CGNAT is **not** included by +#: design; opt in via :data:`EXTRA_CIDRS_ENV`. +DEFAULT_PRIVATE_CIDRS: tuple[IPNetwork, ...] = ( + ipaddress.ip_network("10.0.0.0/8"), + ipaddress.ip_network("172.16.0.0/12"), + ipaddress.ip_network("192.168.0.0/16"), + ipaddress.ip_network("127.0.0.0/8"), + ipaddress.ip_network("169.254.0.0/16"), + ipaddress.ip_network("fd00::/8"), + ipaddress.ip_network("fe80::/10"), + ipaddress.ip_network("::1/128"), +) + + +def is_private_ip(addr: IPAddress, allowlist: tuple[IPNetwork, ...]) -> bool: + """``True`` iff ``addr`` is inside any CIDR in ``allowlist``. + + Type-mismatched comparisons (IPv4 address vs IPv6 network or vice versa) + are skipped silently rather than raising ``TypeError`` — happens any time + the operator's allowlist mixes families. + """ + for net in allowlist: + if isinstance(addr, ipaddress.IPv4Address) and isinstance( + net, ipaddress.IPv4Network + ): + if addr in net: + return True + elif isinstance(addr, ipaddress.IPv6Address) and isinstance( + net, ipaddress.IPv6Network + ): + if addr in net: + return True + return False + + +def resolve_lan_allowed_cidrs(env: dict | None = None) -> tuple[IPNetwork, ...]: + """Compose the effective LAN allowlist: defaults + operator extras. + + Reads :data:`EXTRA_CIDRS_ENV` from ``env`` (defaults to ``os.environ``), + parses with :func:`parse_trusted_proxies` (so the same lenient + "skip-bad-entries" semantics apply), and concatenates with + :data:`DEFAULT_PRIVATE_CIDRS`. Order matters only for log readability; + membership tests are O(n). + """ + source = env if env is not None else os.environ + extra = parse_trusted_proxies(source.get(EXTRA_CIDRS_ENV)) + return DEFAULT_PRIVATE_CIDRS + extra + + +# ── Middleware factory ──────────────────────────────────────────── + + +_REJECT_BODY = { + "error": "forbidden", + "detail": ( + "Source address is not in the configured LAN allowlist. " + "Set PINKY_LAN_EXTRA_CIDRS or change PINKY_DEPLOYMENT_MODE if " + "this is a trusted client." + ), +} + + +def build_lan_filter_middleware( + allowlist: tuple[IPNetwork, ...] | None = None, +): + """Return an ASGI middleware closure enforcing the LAN allowlist. + + The closure inspects each incoming request: + + * If ``request.app.state.deployment_mode`` is **not** ``LAN``, the + request passes through untouched. ``trusted`` and ``web`` modes have + their own enforcement layers (#441 and the reverse-proxy contract). + * Otherwise, the canonical client IP is computed via + :func:`resolve_client_identity` (so XFF parsing matches #442) and + compared against the supplied ``allowlist`` (or the + ``DEFAULT_PRIVATE_CIDRS`` + env extras when ``None``). + * Non-private sources receive ``HTTP 403`` with a JSON body; the + reject is logged at INFO so operators can see scan/probe traffic + without flooding ERROR. + + Args: + allowlist: Optional explicit override. When ``None``, the middleware + recomputes from env on every request — slow path, but useful for + tests that mutate env between calls. ``create_api`` should + always pre-resolve and pass an explicit tuple. + """ + + async def lan_filter( + request: Request, + call_next: Callable[[Request], Awaitable], + ): + state = request.app.state if request.app else None + mode = getattr(state, "deployment_mode", None) + if mode is not DeploymentMode.LAN: + return await call_next(request) + + # Use the cached parsed list when available; fall back to a fresh + # env read so tests don't have to wire app.state. + active_allowlist = allowlist + if active_allowlist is None: + cached = getattr(state, "lan_allowed_cidrs", None) + active_allowlist = ( + cached if cached is not None else resolve_lan_allowed_cidrs() + ) + + trusted_proxies = ( + getattr(state, "trusted_proxies", None) if state else None + ) or () + ci = resolve_client_identity(request, mode, trusted_proxies) + + if not is_private_ip(ci.ip, active_allowlist): + logger.info( + "lan_filter: rejecting %s %s from %s (raw_peer=%s, via_proxy=%s)", + request.method, + request.url.path, + ci.ip, + ci.raw_peer, + ci.via_proxy, + ) + return JSONResponse(_REJECT_BODY, status_code=403) + + return await call_next(request) + + return lan_filter diff --git a/tests/test_deployment_mode.py b/tests/test_deployment_mode.py index dd3a4c06..e3c30836 100644 --- a/tests/test_deployment_mode.py +++ b/tests/test_deployment_mode.py @@ -202,8 +202,13 @@ def test_api_endpoint_exposes_mode(self, tmp_db_path): def test_api_endpoint_exposes_mode_for_each_value(self, tmp_db_path): from pinky_daemon.api import create_api - for mode in DeploymentMode: - app = create_api(db_path=tmp_db_path, deployment_mode=mode) - client = TestClient(app) - resp = client.get("/api") + # In LAN mode the LAN filter (#436) would reject TestClient's + # synthetic peer; opt 0.0.0.0/32 in via env so the test reaches + # /api in every mode. + env = {**os.environ, "PINKY_LAN_EXTRA_CIDRS": "0.0.0.0/32"} + with patch.dict(os.environ, env, clear=True): + for mode in DeploymentMode: + app = create_api(db_path=tmp_db_path, deployment_mode=mode) + client = TestClient(app) + resp = client.get("/api") assert resp.json()["deployment_mode"] == mode.value diff --git a/tests/test_lan_filter.py b/tests/test_lan_filter.py new file mode 100644 index 00000000..2c5a94f9 --- /dev/null +++ b/tests/test_lan_filter.py @@ -0,0 +1,304 @@ +"""Tests for the LAN-only middleware (#436). + +Covers the private-CIDR helper, the env-driven allowlist composer, the +middleware closure (no-op in trusted/web, enforce in lan), and end-to-end +wiring through ``create_api`` (TestClient against an app spun up in each +mode). +""" + +from __future__ import annotations + +import ipaddress +import os +import tempfile +from typing import Any +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from pinky_daemon.config import DeploymentMode +from pinky_daemon.net.lan_filter import ( + DEFAULT_PRIVATE_CIDRS, + EXTRA_CIDRS_ENV, + build_lan_filter_middleware, + is_private_ip, + resolve_lan_allowed_cidrs, +) + +# ── is_private_ip ────────────────────────────────────────────── + + +class TestIsPrivateIp: + @pytest.mark.parametrize( + "addr", + [ + "10.0.0.5", + "172.16.42.1", + "172.31.255.254", + "192.168.1.1", + "127.0.0.1", + "169.254.1.2", + "::1", + "fe80::1", + "fdab:cdef::1", + ], + ) + def test_default_cidrs_accept_private(self, addr): + assert is_private_ip( + ipaddress.ip_address(addr), DEFAULT_PRIVATE_CIDRS + ) + + @pytest.mark.parametrize( + "addr", + [ + "8.8.8.8", + "1.1.1.1", + "203.0.113.7", + "172.32.0.1", # just outside 172.16/12 + "169.255.0.1", # just outside link-local + "100.64.0.5", # CGNAT — explicitly NOT in defaults + "2001:db8::1", # public IPv6 + ], + ) + def test_default_cidrs_reject_public(self, addr): + assert not is_private_ip( + ipaddress.ip_address(addr), DEFAULT_PRIVATE_CIDRS + ) + + def test_cgnat_only_accepted_when_extra(self): + extras = (ipaddress.ip_network("100.64.0.0/10"),) + allowlist = DEFAULT_PRIVATE_CIDRS + extras + assert is_private_ip(ipaddress.ip_address("100.64.0.5"), allowlist) + + def test_ipv4_addr_against_ipv6_net_does_not_crash(self): + only_v6 = (ipaddress.ip_network("fd00::/8"),) + # No match, but importantly: no TypeError. + assert not is_private_ip(ipaddress.ip_address("10.0.0.5"), only_v6) + + def test_empty_allowlist_rejects_everything(self): + assert not is_private_ip(ipaddress.ip_address("10.0.0.5"), ()) + + +# ── resolve_lan_allowed_cidrs ────────────────────────────────── + + +class TestResolveLanAllowedCidrs: + def test_unset_returns_defaults(self): + result = resolve_lan_allowed_cidrs(env={}) + assert result == DEFAULT_PRIVATE_CIDRS + + def test_extra_appended(self): + result = resolve_lan_allowed_cidrs( + env={EXTRA_CIDRS_ENV: "100.64.0.0/10"} + ) + assert result[: len(DEFAULT_PRIVATE_CIDRS)] == DEFAULT_PRIVATE_CIDRS + assert ipaddress.ip_network("100.64.0.0/10") in result + + def test_invalid_extras_skipped(self): + result = resolve_lan_allowed_cidrs( + env={EXTRA_CIDRS_ENV: "garbage,100.64.0.0/10"} + ) + assert ipaddress.ip_network("100.64.0.0/10") in result + # Length = defaults + 1 valid extra; junk dropped. + assert len(result) == len(DEFAULT_PRIVATE_CIDRS) + 1 + + +# ── Middleware closure (unit) ────────────────────────────────── + + +class _FakeApp: + def __init__(self, mode, trusted=(), allowlist=None): + self.state = type("State", (), {})() + self.state.deployment_mode = mode + self.state.trusted_proxies = trusted + if allowlist is not None: + self.state.lan_allowed_cidrs = allowlist + + +class _FakeRequest: + def __init__( + self, + peer="10.0.0.5", + method="GET", + path="/", + headers=None, + app=None, + ): + self.client = type("C", (), {"host": peer})() + self.method = method + self.url = type("U", (), {"path": path})() + self.headers = headers or {} + self.app = app + + +async def _passthrough(_req): + return "ok" + + +class TestMiddlewareModes: + @pytest.mark.asyncio + async def test_trusted_mode_passthrough(self): + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + app = _FakeApp(DeploymentMode.TRUSTED) + # Public IP should still pass — trusted mode is no-op. + req = _FakeRequest(peer="8.8.8.8", app=app) + result = await mw(req, _passthrough) + assert result == "ok" + + @pytest.mark.asyncio + async def test_web_mode_passthrough(self): + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + app = _FakeApp(DeploymentMode.WEB) + req = _FakeRequest(peer="8.8.8.8", app=app) + result = await mw(req, _passthrough) + assert result == "ok" + + @pytest.mark.asyncio + async def test_lan_mode_accepts_private(self): + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + app = _FakeApp(DeploymentMode.LAN) + req = _FakeRequest(peer="192.168.1.10", app=app) + result = await mw(req, _passthrough) + assert result == "ok" + + @pytest.mark.asyncio + async def test_lan_mode_rejects_public(self): + from fastapi.responses import JSONResponse + + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + app = _FakeApp(DeploymentMode.LAN) + req = _FakeRequest(peer="8.8.8.8", app=app) + result = await mw(req, _passthrough) + assert isinstance(result, JSONResponse) + assert result.status_code == 403 + + @pytest.mark.asyncio + async def test_lan_mode_xff_only_honored_when_proxy_trusted(self): + # Untrusted peer claims a private "client" → must NOT pass. + from fastapi.responses import JSONResponse + + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + app = _FakeApp(DeploymentMode.LAN, trusted=()) + req = _FakeRequest( + peer="8.8.8.8", + headers={"x-forwarded-for": "10.0.0.5"}, + app=app, + ) + result = await mw(req, _passthrough) + assert isinstance(result, JSONResponse) + assert result.status_code == 403 + + @pytest.mark.asyncio + async def test_lan_mode_xff_honored_when_proxy_trusted(self): + # Trusted peer can vouch for a private client. + mw = build_lan_filter_middleware(allowlist=DEFAULT_PRIVATE_CIDRS) + trusted = (ipaddress.ip_network("172.16.0.0/12"),) + app = _FakeApp(DeploymentMode.LAN, trusted=trusted) + req = _FakeRequest( + peer="172.16.0.1", + headers={"x-forwarded-for": "10.0.0.5"}, + app=app, + ) + result = await mw(req, _passthrough) + assert result == "ok" + + @pytest.mark.asyncio + async def test_lan_mode_extra_cidrs_via_state(self): + # Operator opted into CGNAT via env → cached on app.state. + cgnat = (ipaddress.ip_network("100.64.0.0/10"),) + custom_allowlist = DEFAULT_PRIVATE_CIDRS + cgnat + mw = build_lan_filter_middleware(allowlist=custom_allowlist) + app = _FakeApp(DeploymentMode.LAN) + req = _FakeRequest(peer="100.64.0.5", app=app) + result = await mw(req, _passthrough) + assert result == "ok" + + @pytest.mark.asyncio + async def test_lan_mode_falls_back_to_env_when_no_allowlist_passed(self): + # No explicit allowlist on the closure or on app.state → middleware + # recomputes from env on each call. + mw = build_lan_filter_middleware(allowlist=None) + app = _FakeApp(DeploymentMode.LAN) + # Make sure no lan_allowed_cidrs is on state. + if hasattr(app.state, "lan_allowed_cidrs"): + del app.state.lan_allowed_cidrs + req = _FakeRequest(peer="10.0.0.5", app=app) + result = await mw(req, _passthrough) + assert result == "ok" + + +# ── End-to-end through create_api ────────────────────────────── + + +@pytest.fixture +def tmp_db_path(): + with tempfile.TemporaryDirectory() as tmpdir: + yield os.path.join(tmpdir, "test.db") + + +class TestCreateApiLanFilter: + def test_trusted_mode_no_filter_applied(self, tmp_db_path): + # Public source → still 200 (the unrelated /api endpoint). + from pinky_daemon.api import create_api + + env = {k: v for k, v in os.environ.items() if k != EXTRA_CIDRS_ENV} + with patch.dict(os.environ, env, clear=True): + app = create_api( + db_path=tmp_db_path, deployment_mode=DeploymentMode.TRUSTED + ) + client = TestClient(app) + # TestClient peer ends up as 0.0.0.0 (unspecified) — would be + # rejected by the LAN filter, but trusted mode is a no-op so it + # should still hit the route. + resp = client.get("/api") + assert resp.status_code == 200 + + def test_lan_mode_default_peer_rejected(self, tmp_db_path): + # TestClient's peer is 0.0.0.0 / "testclient" → not in private + # CIDRs → 403 in LAN mode. + from pinky_daemon.api import create_api + + env = {k: v for k, v in os.environ.items() if k != EXTRA_CIDRS_ENV} + with patch.dict(os.environ, env, clear=True): + app = create_api( + db_path=tmp_db_path, deployment_mode=DeploymentMode.LAN + ) + client = TestClient(app) + resp = client.get("/api") + assert resp.status_code == 403 + body = resp.json() + assert body["error"] == "forbidden" + + def test_lan_mode_extra_cidrs_unblocks_test_client(self, tmp_db_path): + # If we add 0.0.0.0/32 to the allowlist via env, TestClient's + # synthetic peer passes. + from pinky_daemon.api import create_api + + env = {**os.environ, EXTRA_CIDRS_ENV: "0.0.0.0/32"} + with patch.dict(os.environ, env, clear=True): + app = create_api( + db_path=tmp_db_path, deployment_mode=DeploymentMode.LAN + ) + client = TestClient(app) + resp = client.get("/api") + assert resp.status_code == 200 + + def test_web_mode_no_filter_applied(self, tmp_db_path): + # Web mode delegates to the reverse-proxy contract (#441 + bind + # default). The LAN middleware itself must not interfere. + from pinky_daemon.api import create_api + + env = {k: v for k, v in os.environ.items() if k != EXTRA_CIDRS_ENV} + with patch.dict(os.environ, env, clear=True): + app = create_api( + db_path=tmp_db_path, deployment_mode=DeploymentMode.WEB + ) + client = TestClient(app) + resp = client.get("/api") + # /api is unauthenticated; web mode should not block on source. + assert resp.status_code == 200 + + +# Placeholder to keep type-checkers happy on the unused import. +_ = Any