This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 20:56:36 +02:00
1 parent f213d853e2
commit 76b8bb7ee2
78 files changed
+7750 -5204

No files matched your search

+21 -2
View File
@@ -7,6 +7,23 @@ certificate and persist a non-loopback bind; plain remote HTTP requires the expl
startup `--allow-remote-admin` gate. Responses carry no-store, CSP and other browser
protection headers.
## Public-vhost HTTP cache invalidation (opt-in)
`PURGE /path?query` and `BAN /prefix` operate on the normal public vhost
listener and **do not** use an admin session. They are independently denied by
default unless the administrator explicitly populates the cache's
`http_invalidation_allowed_cidrs` allowlist in **Settings → Cache**. Authorization
uses the existing trusted client-IP forwarding logic, vhost/host ACL and rate
limits. BAN is a lazy prefix rule; PURGE evicts an exact URL. Both are restricted
to the request scheme (`http` vs `https`), Host and matched vhost. A shared
two-worker maintenance guard also bounds administrator-driven invalidation;
additional requests receive 429. Ambiguous duplicate control headers and
request bodies are rejected. See
[CACHE](CACHE.md#http-ban--purge-from-authorized-client-ips) for request examples,
scoping, confirmation and reverse-proxy requirements, and
[0.31.0 security review](SECURITY_AUDIT_0.31.0.md) for audit findings. The authenticated
`/api/cache/invalidate` endpoint remains unchanged.
## Browser documentation and anonymous surface
Open `/docs` **on the administration listener** after signing in. The default **Reader**
@@ -72,6 +89,8 @@ framework extraction failures are not guaranteed to have that JSON shape.
| `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans |
| `GET /api/status` | Uptime, active revision, configured counts, listener addresses, feature flags, aggregate metrics, active/desired TCP/UDP streams with per-stream L4 counters, active/desired administration state, TLS policy/cipher inventory and structured-log sink counters |
`GET /api/status?view=live` returns only volatile telemetry suitable for SSE fallback, without the heavy full configuration/OS/runtime objects. `GET /api/system-info` returns cached system diagnostics, host/runtime metadata and memory accounting under `system`, `runtime` and `memory_accounting`. Both are protected by the same cookie/origin admin guard as full status.
`status.streams` contains `active`, `desired`, `restart_required` and a `metrics` map keyed by startup-active stream ID. Stream counters include active/total/completed sessions, rejects, errors, RX/TX bytes and UDP packet counts; the dashboard derives rates from byte-counter deltas.
| `GET /api/logs` | Recent bounded access entries, ring capacity and contention-drop count |
| `GET /api/audit` | Last 100 audit entries; database retention is 10,000 |
@@ -83,9 +102,9 @@ Except for health and authentication/bootstrap routes, these endpoints require a
session. These administrative previews are not anonymous. The separate configurable
exporter uses its own CIDRs and optional bearer token; see the scrape section below.
SSE has eight viewer slots, generates a snapshot approximately every two seconds
SSE has eight viewer slots, generates a snapshot every 1/2/5/10 seconds per authenticated client (via `?interval=`, default 2)
and rechecks session validity. It does not queue an unlimited per-viewer event log
or guarantee replay. The browser falls back to polling. An active dashboard renews
or guarantee replay. The browser falls back to compact status/log polling only after SSE fails and stops polling after its recovery. Invalid SSE intervals are rejected with 422. An active dashboard renews
idle activity, not the absolute session deadline.
Configuration history, introduced in SQLite schema 8 and retained in current schema 9, keeps the newest 512 successful manual/configuration revisions. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection. `POST /api/config/history/restore` also requires the normal CSRF/request-marker protections, rejects stale `expected_revision`, and never rewinds the revision counter: restoring revision 85 while current revision is 120 creates revision 121.
+51 -4
View File
@@ -13,7 +13,7 @@ different vhost/config scopes internally and uses response `Vary` to select repr
variants.
This is not a VCL implementation. Generic Varnish grace, ESI, public unauthenticated
BAN/PURGE and distributed cache state are not implemented. RustProxy 0.22.1 adds bounded
unrestricted public BAN/PURGE and distributed cache state are not implemented. RustProxy 0.22.1 adds bounded
foreground request collapsing, authenticated selective PURGE/BAN and optional proactive
refresh while keeping directive-controlled stale delivery through RFC 5861 and request
`max-stale`; it is not an unconditional availability/grace layer. RustProxy also keeps its
@@ -36,6 +36,48 @@ proxy origins, range requests and conditional requests can reuse fresh stored
representations. Do not rely on stale-if-error remote-backend refresh behavior
for local Web Server routes.
## HTTP BAN / PURGE from authorized client IPs
The **Settings → Cache → HTTP BAN / PURGE IP allowlist** control sets
`cache.http_invalidation_allowed_cidrs`. The default is **empty (disabled)**.
Allow individual client IPs or small CIDR ranges, one per line. Saves validate
IPv4/IPv6 CIDR syntax and reject `/0` networks. An allowed request must also
pass the vhost listener binding, global/vhost ACL, and rate limits. The
operation is restricted to the matched external HTTP/HTTPS scheme, `Host` and
vhost; it cannot choose other tenants or the opposite scheme through headers. Commands use the existing
cache invalidation engine, are audit-logged before execution and share a
two-worker maintenance limit with authenticated admin invalidation (429 when
busy). They are never forwarded upstream.
- `PURGE /path?query` immediately invalidates exact cached representations of
that URL. Returns `200` with JSON result/count. Other query variants are not
matched.
- `BAN /prefix` records a lazy prefix ban of older cached representations
for that host/vhost. Returns `202` and ban ID, normally expires after one hour.
Optional `X-RustProxy-Ban-Ttl: 30` sets 1..604800 seconds. Newly filled
representations are allowed (Varnish-like lazy BAN).
- `BAN /` requires `X-RustProxy-Confirm-Wide: 1` to prevent accidental
whole-host invalidation. Request bodies, upgrades, arbitrary selectors and
wildcard cross-vhost operations are rejected.
Examples from an allowlisted machine (replace the domain and path):
```sh
curl -i -X PURGE 'https://site.example/assets/app.js?v=3'
curl -i -X BAN 'https://site.example/assets/'
curl -i -X BAN 'https://site.example/' -H 'X-RustProxy-Confirm-Wide: 1'
```
**Behind a reverse proxy:** forward nonstandard `BAN`/`PURGE` methods to the
RustProxy public listener; do not let an upstream cache intercept them. Configure
RustProxy's existing `forwarding.trusted_proxies` accurately and ensure the
edge sanitizes/replaces untrusted `Forwarded`/`X-Forwarded-For` headers. When a
trusted proxy does not provide a usable real-client chain, BAN/PURGE fails
closed. Do **not** allowlist an entire proxy NAT network unless all its users
should have destructive cache access. Unlike full Varnish VCL, this interface
intentionally provides only exact PURGE and bounded URL-prefix BAN, not an
arbitrary untrusted regex/VCL expression.
## Settings
| Field | Default | Bounds / meaning |
@@ -66,6 +108,7 @@ for local Web Server routes.
| max_ttl_seconds | 86400 | 1..604800; maximum configured retention/freshness cap |
| bypass_query | false | Bypass requests containing a query when enabled |
| bypass_prefixes | [] | Up to 64 normalized absolute path prefixes |
| http_invalidation_allowed_cidrs | [] | Opt-in IP/CIDR allowlist (max 32) for HTTP BAN/PURGE. Empty blocks all HTTP methods; `/0` is invalid. |
| static_ignore_cookies | true | For configured static extensions only, ignore request Cookie for eligibility/keying |
| expose_status | false | Emit `X-Cache: HIT`, `MISS` or `BYPASS` |
| expose_hits | false | Emit per-object `X-Cache-Hits` |
@@ -175,9 +218,13 @@ Cache invalidation remains explicit:
- regex rules and prefix `/` are treated as broad operations and require explicit confirmation,
- only cache-compatibility changes advance the normal cache generation/revision; unrelated logging/TLS/admin/metrics/response-presentation saves retain objects, and cache request-bypass edits plus response-only `X-Cache`/`Cache-Status`/resident-age/`Via` diagnostics also apply without purging stored objects.
There is no public unauthenticated PURGE/BAN endpoint. If an application changes a GET
resource after POST/PUT/PATCH/DELETE, use origin freshness/validators or the authenticated
administration invalidation path; RustProxy does not guess the affected URLs.
Unrestricted anonymous cache-maintenance access is not exposed: the public
HTTP BAN/PURGE interface is explicitly IP/CIDR-authorized (empty by default),
separate from the administrator-only selective invalidation API. If an
application changes a GET resource after POST/PUT/PATCH/DELETE, use origin
freshness/validators or one of the explicit invalidation paths; RustProxy
does not guess the affected URLs. For exposure review and test scenarios, see
[0.31.0 security audit](SECURITY_AUDIT_0.31.0.md).
## Eligibility and freshness
+6
View File
@@ -7,6 +7,12 @@
# Changelog
## 0.31.0 Axum fallback handler E0277 source correction
- Correct the reported `Handler<_, _>` error at `src/proxy/types.rs:53` after the public cache-maintenance path was added. The asynchronous `cache_http_invalidation` helper previously accepted `&Request<Body>` while awaiting database audit and cache invalidation; Axum's body is not `Sync`, so the borrowed request can make the `forward` handler future non-`Send`.
- Pass ownership of `Request<Body>` to the maintenance helper, perform the same body/header/allowlist validation, copy the HTTP method for the audit entry, and release the request body before the first asynchronous operation. The `forward` router and public API semantics are unchanged.
- Add an offline source regression asserting that the async helper owns its request. Pre-Rust `./dev.sh test` checks and Go tests pass; full Rust parsing, type checking, tests and runtime maintenance calls cannot be validated without Cargo/Rustc.
## 0.30.1 same-version sysinfo compiler feature correction
- Fix four compiler `E0433` errors in the system information endpoint by enabling `sysinfo`'s `system` feature alongside `network` with `default-features = false` unchanged. No dependency version or API change.
+152
View File
@@ -0,0 +1,152 @@
# RustProxy 0.31.0 – implementation and verification report
### Security/compilation follow-up
- Correct the user-reported `E0716` lifetime error in the HTTP BAN/PURGE
error path by returning an owned-independent static response and logging the
detailed validation error.
- Restrict invalidation to the selected HTTP/HTTPS scheme, Host and vhost;
reject duplicate maintenance headers, invalid bodies and broad BAN without
confirmation. Execute shared cache maintenance in a bounded blocking pool
of two operations, rather than scanning on Tokio core workers. Audit
authenticated BAN-rule deletion before mutation.
- Add localized BAN/PURGE `curl` help next to the Cache allowlist and Rust
API/auth/security regression tests. Full review, exposure matrix and live
test plan: [SECURITY_AUDIT_0.31.0](SECURITY_AUDIT_0.31.0.md). Source-level
coverage is **not** Rust runtime/compile verification.
### Panel compression and cache HTTP invalidation follow-up
The 0.31.0 maintenance follow-up introduces one shared, precompressed panel
content handler for authenticated HTML, CSS, JS and language bundles (negotiated
Zstd/Brotli/gzip with identity fallback; settings under Administration), plus
optional IP/CIDR-authorized public `BAN`/`PURGE` scoped to the selected vhost.
Existing cache invalidation, revisioned settings and auth/ACL behavior are reused.
The System information Refresh button now sits next to the API badge. See
[SETTINGS](SETTINGS.md), [CACHE](CACHE.md) and [TEST_REPORT](TEST_REPORT.md) for
behavior and verification limitations. This note does not assert a Rust build.
Date: 2026-10-10. Input: user-provided `RustProxy-0.30.1.zip`, which **does not contain `.git` metadata**. This is a complete edited source snapshot, **not** a verified checkout of remote `master` or a compiled/released binary. No commit or tag was created.
## Changes implemented
1. **One dashboard coordinator:** `src/admin/web/app/refresh.js` replaces recurring independent panel timers. Every task has a stable ID, active-view guard, sequential `setTimeout` scheduling after completion, `AbortController`, request epoch/generation guards, independent error handling, and manual execution. Auto polling and SSE pause on hidden tabs and stop on logout; timers and `visibilitychange` listener are removed.
2. **SSE with fallback:** `/api/events?interval=1|2|5|10` preserves one shared live channel per browser with individually selected frequency, session checking and heartbeats. It carries lightweight status and the existing log snapshot. `/api/status?view=live` and `/api/logs` polling run only while SSE is unavailable, and are stopped on recovery. The full `/api/status` remains available to existing clients.
3. **Traffic / Cache:** retained `/api/traffic` and `/api/cache`; they poll only in their active view, while both per-view Auto and account Live switches allow it. The interval comes from per-account preferences. `0` selects manual operation without disabling refresh buttons, paging, charts, filters or operations.
4. **System diagnostics:** new authenticated `/api/system-info` sourced from cached `sysinfo`/diagnostic and existing ACME-runtime metadata; OS identification is cached. Normal live snapshots no longer include costly system information, memory accounting, certificate runtime details or full configuration. System info polls at the selected lower cadence for Overview and the relevant Settings tabs; manual refresh is always available.
5. **Preferences and UI:** existing `/api/preferences` and account-specific SQLite storage extended with defaulted `refresh` settings; strict server/browser allowlists; UI controls, reset, persistence and immediate rescheduling. English/Polish translations maintained. Old preference documents are accepted without migration.
6. **Release:** main Cargo manifest/lock package, OpenAPI/version checks and active docs identify `0.31.0`. Historical changelog entries and third-party crate version numbers remain intact. Existing SQLite schema 9, cold-cache format 3 and proxy data path are unchanged.
## Refresh defaults and boundaries
| Key | Default | Allowed values (seconds) |
|---|---:|---|
| `live_enabled` | `true` | `true`, `false` |
| `overview_seconds` | 2 | 1, 2, 5, 10 |
| `traffic_seconds` | 5 | 0, 3, 5, 10, 30 |
| `cache_seconds` | 5 | 0, 3, 5, 10, 30 |
| `system_info_seconds` | 60 | 0, 15, 30, 60, 300 |
`live_enabled=false` pauses **all automatic** fetching, including SSE and system info. Every manual refresh remains possible. Server stream cadence is client-local, not global. The existing `/api/live-logs` remains independent, in accordance with its existing logs subscription model.
## Verification executed
| Action | Result | Scope/limit |
|---|---|---|
| `./dev.sh test` | **Partial, exit 127** | All Rust-independent preflight, source/module/security/version checks, SQLite schema-9 migration/upgrade, 8 Node UI tests (including new refresh test), docs and language checks passed. **Rust toolchain check then failed**: `cargo`/`rustc` unavailable. |
| `node tools/checks/refresh_0310_ui_test.mjs` | **PASS** | Deterministic simulated browser clock: 2/3/5/0-s cadence, manual mode, Live toggle, 20 rapid tab transitions, no duplicate requests/timers, aborting obsolete work, hidden/visible, SSE error/recovery fallback, independent failures, logout/cleanup, browser preference validation. |
| `go test ./...` (in `docker-agent/`) | **PASS** | Go agent unit tests. |
| `node --check` over all `src/admin/web/**/*.js` | **PASS** | JavaScript syntax only, not execution in Chrome/Firefox. |
| `python3 tools/checks/check_source.py` | **PASS** | Static contracts (not Rust type checking). |
| `python3 tools/checks/release_smoke.py` | **PASS** | Offline SQLite/migration and release integration/source contracts. |
| `bash -n dev.sh` and Python compile checks | **PASS** | Shell and Python syntax. |
| `./dev.sh check`, `./dev.sh fmt`, `./dev.sh release` | **NOT EXECUTED (exit 127)** | Rust/Cargo/rustfmt absent. No format, typecheck, Rust unit/integration suite, linked release binary. |
| Live Rust SSE/API/Traffic/Cache + multi-user persistence | **NOT EXECUTED** | Cannot start the Rust service in this environment. Rust integration regression tests were added to source but not run. |
| Real bandwidth/serialized SSE bytes/CPU, long-running resource metrics | **NOT MEASURED** | No Rust server. The repo's 2,000 HTTP-request memory-load smoke uses a Python **peer**, not the RustProxy binary; it is not RustProxy performance evidence. |
**Important:** all Rust runtime, compile and real end-to-end security, SSE heartbeat/reconnect and production proxy regressions remain open; this source archive cannot be declared production-ready before those checks.
## Request-rate analysis (source-derived forecast, not wire measurement)
Rates below are **steady-state theoretical** at default intervals, for a single visible administrator with no manual clicks; the first fetch after entering a view is extra. They are not byte-size/CPU benchmark results.
| Mode | Previously | v0.31.0 default |
|---|---|---|
| Overview status + logs | ~30 full-status SSE snapshots/min (each contained system data), with log payload | ~30 compact SSE snapshots/min with logs; no concurrent fallback |
| Overview system diagnostics | embedded in each SSE status | ~1 GET `/api/system-info`/min |
| Traffic active | ~20 GET `/api/traffic`/min at 3 s | ~12 GET/min at 5 s |
| Cache active | ~20 GET `/api/cache`/min at 3 s | ~12 GET/min at 5 s |
| Other views / hidden tab | older per-view guard behavior; background SSE not globally paused | zero scheduled Traffic/Cache; hidden tab closes SSE and pauses all automatic polling |
| Settings not requiring diagnostics | independent legacy status data carried by SSE | no automatic GET `/api/system-info` |
Actual SSE payload lengths, encoding/cost, network traffic and memory footprint **must be measured against a running binary**. No byte reduction percentage is asserted.
## Changed files (40 edited, 8 new, 0 deleted)
- `AGENTS.md`
- `CHANGELOG.md`
- `Cargo.lock`
- `Cargo.toml`
- `README.md`
- `ROADMAP.md`
- `SHA256SUMS`
- `dev.sh`
- `docs/API.md`
- `docs/README.md`
- `docs/SETTINGS.md`
- `docs/TEST_REPORT.md`
- `langs/en_US.json`
- `langs/pl_pl.json`
- `src/admin/documentation/documents.rs`
- `src/admin/logging_api.rs`
- `src/admin/openapi.json`
- `src/admin/preferences.rs`
- `src/admin/router.rs`
- `src/admin/status.rs`
- `src/admin/tests.rs`
- `src/admin/tests/part_04.rs`
- `src/admin/web/acme.js`
- `src/admin/web/app/core.js`
- `src/admin/web/app/preferences.js`
- `src/admin/web/app/runtime.js`
- `src/admin/web/cache-panel.js`
- `src/admin/web/docker.js`
- `src/admin/web/drafts.js`
- `src/admin/web/index.html`
- `src/admin/web/settings.js`
- `src/admin/web/traffic-panel.js`
- `src/admin/web/transform-panel.js`
- `tools/checks/check_source.py`
- `tools/checks/docs_check.py`
- `tools/checks/preflight.py`
- `tools/checks/release_smoke.py`
- `tools/checks/system_info_0301_ui_test.mjs`
- `tools/checks/system_info_no_subprocess_contracts.py`
- `tools/dev/memory_load.py`
- `docs/IMPLEMENTATION_0.31.0.md`
- `src/admin/preferences_columns.rs`
- `src/admin/refresh_preferences.rs`
- `src/admin/status_live.rs`
- `src/admin/status_system.rs`
- `src/admin/tests/part_09.rs`
- `src/admin/web/app/refresh.js`
- `tools/checks/refresh_0310_ui_test.mjs`
## Required remaining release gate
On a host with stable Rust toolchain and a configured development environment, run:
```sh
./dev.sh fmt
./dev.sh check
./dev.sh test
./dev.sh release
cd docker-agent && go test ./... && cd ..
```
Test real session login and per-account persistence, permission denial for `/api/system-info`, SSE connect/heartbeat/reconnect/disconnect, active-vs-hidden browser polling via DevTools, real `/api/status` backwards compatibility and live/proxy/cache/traffic behavior. Measure serialized SSE bytes and generation time, CPU and request counts by Overview/Traffic/Cache/Settings/hidden tab. Correct any Rust compilation/runtime failures before deploying or creating a release tag.
## Same-version mobile startup follow-up (2026-10-10)
The authenticated dashboard replaces the previous all-body `visibility: hidden` with a visible CSS-only spinner, a 12-second stalled-start hint with reload link, and an EN/PL explicit failure screen. It reveals the shell and starts the existing refresh coordinator before awaiting noncritical preferences/configuration/status; failed configuration cannot hide the shell. `Refresh now` is now aligned with the Overview heading. The fixed authenticated HTML is gzip-compressed once into cached bytes, negotiated per request; unchanged identity responses and public login remain. Browser UI/source checks are documented in `docs/TEST_REPORT.md`; Rust compilation and real wire/mobile tests remain open.
+3 -1
View File
@@ -31,6 +31,8 @@ This is the canonical index for the current project documentation. The authentic
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
- [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing.
- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone.
- [0.31.0 security audit](SECURITY_AUDIT_0.31.0.md) - current API/panel/BAN/PURGE attack-surface review, hardening, compile fix and mandatory runtime gates.
- [0.31.0 implementation report](IMPLEMENTATION_0.31.0.md) - consolidated changes, exact file inventory, validation results and outstanding Rust runtime acceptance.
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
## Functional documentation
@@ -78,7 +80,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md); [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md); [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
+148
View File
@@ -0,0 +1,148 @@
# RustProxy 0.31.0 - API, panel and HTTP cache-maintenance security review
Scope: source snapshot supplied for the v0.31.0 maintenance follow-up, reviewed
2026-10-10. This is a **code/contract review with offline regressions**, not a
penetration test or proof of production safety. The supplied archive has no
`.git` directory; the review does not establish parity with remote `master`.
## Interfaces and trust boundaries
| Surface | Exposure | Authentication and mutation rules | Assessment |
|---|---|---|---|
| Public administration bootstrap (`/`, login JS/CSS, `/api/auth/status`, setup/login POST) | Login/public administration listener | Host/origin validation; request marker for unsafe bootstrap actions; one-time setup capability and login throttling | Source-reviewed; live browser flow pending |
| Private panel HTML, JS, documentation, OpenAPI | Administration listener | Administrator session; `Cache-Control: no-store`; CSP, frame and MIME protections | Source and anonymous-response test reviewed; runtime pending |
| `/api/status`, `/api/system-info`, `/api/traffic`, `/api/cache`, logs, config, preferences, audit | Administration listener | Administrator cookie session; sensitive reads not on anonymous allowlist | Source and regression tests added; runtime pending |
| `/api/events`, live-log SSE | Administration listener | Session on subscribe and repeated validity checks; bounded viewers; heartbeat/disconnect cleanup | Source-reviewed; reconnect/load pending |
| State-changing admin APIs (including cache/config/preferences) | Administration listener | Valid session, Origin/Host, `X-RustProxy-Request: 1`, CSRF token; audit on cache commands | Source and regression tests added; runtime pending |
| Certificate API (`/api/certificates/{id}/reload`, deploy) | Administration listener | Narrow-scoped Key ID + Bearer grant; no general read access; HTTPS required remotely; origin validation when supplied | Source-reviewed; token rotation/revocation live tests pending |
| Public `BAN`/`PURGE` | Configured public vhost listener | **No admin session**: separate explicit IP/CIDR allowlist (empty by default), matched listener/vhost/ACL and optional rate limit | Hardened below; reverse/HTTP tests pending |
| Separate Prometheus metrics exporter | Distinct optional listener | Configured IP CIDRs and bearer-token policy; not the admin session | Unchanged; deployment exposure audit pending |
## Findings addressed in this follow-up
1. **E0716 / compilation defect (high functional impact).** `error()` accepts
`&'static str`; a reference to `format!(...)` was a temporary. Invalid
selector errors now log the detailed reason server-side and return a fixed,
non-sensitive message with status 422. This removes the reported lifetime
violation; the Rust compiler must still verify the entire crate.
2. **Cache protocol-scope confusion (medium).** Path-only `BAN`/`PURGE`
selectors could affect representations of the same host/path across both
HTTP and HTTPS. Targets are now built with the trusted external scheme,
validated to `http`/`https`, and the matched Host/vhost. IPv6 literal Hosts
are bracketed for the URL parser. Other tenants, schemes and hosts remain
outside the selector.
3. **Unbounded blocking maintenance work (medium availability risk).** Cache
invalidation may scan hot/cold entries. Both public HTTP and authenticated
admin cache purge/selective invalidation share two maintenance slots and
execute scans with `spawn_blocking`. Bursts receive 429 instead of piling
up unbounded blocking work on Tokio core threads. The permit stays with the
worker after a client disconnects. This is a concurrency bound, **not** a
per-client rate quota or a hard time limit for disk work.
4. **Ambiguous maintenance input (medium).** Duplicate TTL/confirmation or
`Content-Length` headers, transfer-encoded bodies, nonempty bodies and
upgrade requests are rejected; `Content-Length: 0` remains compatible with
CLI clients. TTL is limited to 1..604800 s. Broad `BAN /` requires the
explicit confirmation header; `PURGE` cannot use BAN-only controls.
5. **Audit ordering (low/medium).** Removal of an administrator-created BAN
rule now records the audit entry before mutating state. Unavailable audit
storage blocks the mutation rather than allowing an unaudited removal.
6. **Operator safety/usability (low).** An EN/PL help popover beside the HTTP
allowlist gives exact `curl` syntax, IP-only authentication semantics and
reverse-proxy identity requirements. No new global config variable is added.
## Existing protections checked in source
- Admin router's exact anonymous method/path allowlist contains only the
expected authentication bootstrap APIs; private docs and assets remain
session-protected. Request guard rejects duplicate Host/Origin and checks
browser `Sec-Fetch-Site` cross-site signals.
- Mutation guard uses an Origin/Host match, per-session CSRF comparison and
`X-RustProxy-Request` marker; cookie sessions are HttpOnly/SameSite=Strict
and use Secure under HTTPS/trusted forwarded protocol.
- Secure response policy includes CSP, `X-Frame-Options: DENY`,
`X-Content-Type-Options: nosniff`, referrer restrictions and no-store for
private material. A static scan checked 27 first-party browser JS files for
direct unsafe HTML injection and dynamic `eval`/`Function` sinks; this scan
**cannot establish that the entire DOM is XSS-free**.
- IP trust comes from the transport peer or a validated forwarding chain only
when the peer is configured trusted. An untrusted client-supplied XFF does
not independently grant BAN/PURGE permission. The cache allowlist rejects
`/0`, invalid CIDRs, duplicates and more than 32 entries. Requests are never
forwarded upstream.
- Certificate API tokens have explicit operation/ID grants and cannot use the
general admin read API; Bearer grant operations are intentionally separate
from cookie + CSRF authentication.
## HTTP maintenance CLI (allowlisted client only)
```sh
# One URL, including the query string: 200 with JSON result.
curl -i -X PURGE 'https://site.example/assets/app.js?v=3'
# Lazily ban previously cached objects under this path: 202 with JSON result.
curl -i -X BAN 'https://site.example/assets/'
# Entire host: explicit opt-in confirmation header.
curl -i -X BAN 'https://site.example/' -H 'X-RustProxy-Confirm-Wide: 1'
# Optional BAN expiration (30 seconds).
curl -i -X BAN 'https://site.example/assets/' -H 'X-RustProxy-Ban-Ttl: 30'
```
Use **Settings -> Cache -> HTTP BAN / PURGE IP allowlist** to grant the
caller IP/CIDR. This HTTP interface does not accept admin cookies/tokens as an
alternative to the allowlist. The domain, Host header, external scheme and
listener must select the intended vhost. Behind a reverse proxy, forward the
non-standard methods untouched and **replace**, rather than append untrusted,
XFF/Forwarded headers at the edge. Never add the entire reverse-proxy subnet
as a maintenance client merely because it is trusted for forwarding.
## Release security test plan (requires compiled RustProxy)
1. On the deployment host, run `cargo fmt --all -- --check`,
`./dev.sh check`, `./dev.sh test`, and `./dev.sh release`. Address any
compiler/regression errors before installation. `cargo audit` or an
equivalent advisory scan requires an up-to-date advisory DB and was not run
in this offline environment.
2. Start a disposable instance with a populated cache under both HTTP and
HTTPS for the same vhost/URL. Confirm `PURGE` returns 200 and drops the
exact query variant only; `BAN` returns 202 and invalidates only **older**
entries on the URL prefix. Confirm the *other* scheme and vhost stay hot.
3. From a denied IP, expect 403 on `BAN` and `PURGE`. With an empty allowlist,
all clients must be denied. Send duplicate TTL/confirmation/Content-Length,
TE/body and `BAN /` without confirmation: expect 400. On a trusted-proxy
hop with missing real client identity, expect 403. Test both IPv4 and IPv6.
4. From an allowed IP, send 10 concurrent broad maintenance requests; confirm
that only two workers run simultaneously and excess return 429. Verify
server CPU/IO and audit entries while the cold tier is populated.
5. Send GET requests without admin cookies to `/api/config`, `/api/status`,
`/api/system-info`, `/api/events`, `/api/logs`, `/docs/openapi.json` and
`/assets/app.js`; expect 401 and protective headers. With valid cookie but
missing/bad Origin, marker or CSRF on PUT/POST/DELETE, expect 403. Test
logout and password/session invalidation, live SSE reconnect/expiry.
6. Test the Certificate API with no grant, a valid certificate-specific grant,
revoked token, HTTP remote peer, and misuse against `/api/config`/logs.
Check errors for token disclosure and audit coverage.
7. Test actual browser startup, CSP console, output escaping and form save
with malicious HTML strings; inspect reverse-proxy Host/Origin forwarding.
Prefer HTTPS administration with a strict `allowed_hosts` list.
## Residual risk / non-claims
- **Not executed in this environment:** Cargo format/check/tests/release,
actual Rust listener requests, HTTP/2/3, fuzzing, penetration tests,
session revocation under load, proxy deployment configuration, dependency
CVE scanning and long-running cold-cache resource measurements.
- Proxy trust configuration is security-critical. A misconfigured edge that
accepts forged forwarded client addresses can let external users impersonate
an allowlisted IP. Restrict direct listener reachability and sanitize headers.
- A public IP allowlist is not user authentication; anyone using an allowed
IP/network can issue destructive maintenance commands. Use narrow CIDRs and
optionally enforce additional controls on the edge (mTLS, firewall).
- The two-worker concurrency bound mitigates bursts, not all denial-of-service
scenarios: configured cache size and storage latency still determine scan
cost. Per-IP quotas are not provided here.
- Proxy protocol path, certificate verification, arbitrary upstream code,
all application content and third-party packages are **not certified safe**
by this audit. Production acceptance remains open until the runtime test
plan is completed.
+47 -3
View File
@@ -12,9 +12,25 @@ The Settings page includes a local search field that filters category tabs using
Unfinished Web Server, Vhost, Redirect and Stream forms use a session-only draft dock. Closing a dirty editor minimizes its exact browser state to the bottom-right dock so another editor can be opened independently. The dock follows the square-corner panel styling of the dashboard and intentionally avoids rounded/glass presentation. Drafts are intentionally not written to SQLite because form state can contain credentials or other secrets; they are discarded on logout/session reset.
## Refresh preferences (0.31.0)
**Settings -> Appearance -> Data refresh** saves an additional `refresh` object in the existing `admin_preferences` document for the signed-in account. It does not modify proxy configuration or create any migration. Older documents, including partially populated `refresh` objects, use defaults. Browser and server enforce exact allowed values:
| Preference | Default | Allowed seconds |
|---|---:|---|
| `refresh.live_enabled` | `true` | `true`, `false` |
| `refresh.overview_seconds` | 2 | 1, 2, 5, 10 |
| `refresh.traffic_seconds` | 5 | 0, 3, 5, 10, 30 |
| `refresh.cache_seconds` | 5 | 0, 3, 5, 10, 30 |
| `refresh.system_info_seconds` | 60 | 0, 15, 30, 60, 300 |
`live_enabled=false` disables automatic SSE **and all polling**, including system information. `0` disables only that source's automatic polling. Overview and System information have **Refresh now** buttons; Traffic and Cache retain their own buttons, filters, charts and pagination. The per-view Auto checkboxes for Traffic/Cache/Transform and the Docker live switch remain independent opt-outs, and none changes another administrator's preferences. **Reset section** restores refresh defaults without changing theme or unrelated preferences.
The main `/api/events?interval=N` connection (allowlisted 1/2/5/10, default 2 seconds) carries compact live status plus the existing bounded log snapshot. The server checks the session on every event and retains its heartbeat, viewer cap and normal disconnect cleanup. On an SSE error the browser polls `/api/status?view=live` and `/api/logs` until snapshots resume, not concurrently during healthy streaming. The old full `/api/status` response is kept for API clients and initial diagnostics. The separate authenticated `/api/system-info` reuses cached diagnostics and ACME metadata, never runs diagnostic subprocesses per request, and is fetched automatically only for Overview or the relevant Settings pages. A hidden browser tab pauses the channel and all automatic polling; returning revalidates its session before normal operation. All interval changes take effect immediately, without reloading or changing any server-global cadence.
## System information (0.30.1)
**Settings -> System information** (`/settings?tab=system-info`) is a read-only view of the authenticated `/api/status` endpoint. It reports the OS name/kernel on Linux when available, executable architecture, logical CPU count, CPU and RAM telemetry, host/process uptime, RustProxy version, process RSS, thread and file-descriptor counts, and the existing statistics source and data/database locations. Values not supported by the host/platform display as unavailable, never as zero.
**Settings -> System information** (`/settings?tab=system-info`) is a read-only view of the authenticated `/api/system-info` endpoint (and the initial, backwards-compatible full `/api/status` response). It reports the OS name/kernel on Linux when available, executable architecture, logical CPU count, CPU and RAM telemetry, host/process uptime, RustProxy version, process RSS, thread and file-descriptor counts, and the existing statistics source and data/database locations. Values not supported by the host/platform display as unavailable, never as zero.
ACME diagnostics formerly shown in About and under Administration are now on this tab: mode/state, certificate environment path, and the active Python/pip/Certbot versions. Versions are probed by the ACME runtime setup with bounded subprocess output/timeouts and published from stored runtime status. They are not queried at every page refresh or status API request. Failed/unavailable probes leave the version empty. No new persistent settings or permissions are introduced.
@@ -98,11 +114,34 @@ accepted only when `administration.tls` is true. Plain remote HTTP remains avail
"tls": true,
"certificate_id": "admin-example",
"allowed_hosts": ["admin.example.com"],
"session_days": 30
"session_days": 30,
"panel_compression": {"zstd": true, "br": true, "gzip": true}
}
}
```
**Administration → Panel content compression** (next to the admin listener/TLS settings)
controls Zstd, Brotli and gzip for the authenticated HTML document, JavaScript,
CSS and language bundles. Each browser request is negotiated using
`Accept-Encoding` quality values: the highest positive `q` wins, and ties
prefer **Zstd → Brotli → gzip**. Codecs can be enabled independently; the
identity representation is returned when none is acceptable or all are off.
The source is encoded only once per embedded asset and shared across visitors.
Settings persist in the existing revisioned administration document, without
additional CLI flags or a separate environment/config variable, and take effect
without restart. Each compressed response includes `Content-Encoding`,
`Vary: Accept-Encoding` and the representation-specific `Content-Length`.
The response compression of proxied websites remains independent; API responses,
SSE and WebSocket streams are not affected.
**Reverse proxy:** an upstream may remove or replace `Accept-Encoding`. RustProxy
negotiates from the header it actually receives, not the original browser request.
A reverse proxy normally passes through already encoded responses instead of
recompressing them; if the reverse should exclusively manage encoding, configure
it to strip `Accept-Encoding` on requests upstream, or turn the panel codecs off.
This provides compatible HTTP negotiation but does not guarantee identical
compression with every reverse proxy configuration.
`certificate_id: null` uses `data/certs/default.pem` and `default.key`, generated by
the application. A selected inventory certificate is loaded from the same private
certificate directory. `session_days` controls the absolute browser-session lifetime
@@ -169,8 +208,13 @@ Version 0.22 adds foreground fill collapsing (`collapse_enabled`,
(`proactive_refresh_enabled`, `proactive_refresh_before_expiry_seconds`,
`proactive_refresh_percent`, `proactive_refresh_max_inflight`). Defaults preserve the
previous stale-if-error behavior; proactive refresh is disabled by default. Selective
PURGE/BAN is an operational API/panel action rather than persisted configuration. See
PURGE/BAN is available in the authenticated API/panel, and HTTP BAN/PURGE has an optional, separately stored CIDR allowlist (`cache.http_invalidation_allowed_cidrs`, empty by default). See
[CACHE.md](CACHE.md) for request-collapse, Range and invalidation semantics.
The adjacent **info (`i`) popover** in Settings → Cache shows ready-to-copy
`curl -X PURGE` and `curl -X BAN` examples in English and Polish, including
the explicit `BAN /` confirmation. These public methods authorize by the
source IP/CIDR only; they do not accept a browser admin session as a bypass.
See [0.31.0 security audit](SECURITY_AUDIT_0.31.0.md) for reverse-proxy trust and tests.
## Static-response transformation
+41 -1
View File
@@ -1,4 +1,44 @@
# Verification report - RustProxy 0.30.1
# Verification report - RustProxy 0.31.0
## 0.31.0 Axum handler `Send` correction (2026-10-10)
- **Reported failure:** `error[E0277]` at `src/proxy/types.rs:53`, `.fallback(forward)`, on Axum 0.8.9. `forward` calls asynchronous public BAN/PURGE handling. That helper accepted `&Request<Body>` across `.await`, which requires a `Sync` body to produce the `Send` future that `Handler` expects.
- **Source correction:** move `Request<Body>` into `cache_http_invalidation`, validate it synchronously, copy the method for audit logging and explicitly drop the body before the first `.await`. No cache selectors, IP permissions, status codes, API routes or session flows were changed. Strengthen `tools/checks/admin_surface_audit_0310.py` to reject a reintroduced borrowed request in the async handler.
- **Executed:** `./dev.sh test` — all pre-Rust checks **PASS** (source/security/SQLite, UI Node, docs, language packs); command terminates **127** when Cargo is required. `./dev.sh check` — **127**, Cargo absent. `cd docker-agent && go test ./...` — **PASS**. `bash -n dev.sh`, Python source compile and manifest version probe — **PASS** (`0.31.0`).
- **Not executed:** Rust `cargo check --locked --all-targets`, `cargo test --locked --all-targets`, `cargo build --locked --release`, rustfmt or live HTTP regression. No `cargo`/`rustc` or download access is available in this environment. This source correction is not claimed as a compiler-verified release.
## 0.31.0 BAN/PURGE compilation fix, security review and UI help (2026-10-10)
- Addressed reported Rust E0716 (`format!` temporary borrowed as `&'static str`) by returning a fixed 422 response and logging the specific rejection.
- Restricted public HTTP maintenance to one external scheme and matched Host/vhost, rejected duplicate controls and invalid bodies, and added a shared two-slot limit plus `spawn_blocking` for heavy HTTP/admin cache invalidation work; audit removal before BAN-rule deletion.
- Added localized EN/PL `curl` help popover beside the Cache allowlist; added protocol/host/scheme/headers Rust regression sources, admin anonymous/CSRF test sources and an offline source audit script. The detailed findings, attack-surface table and operator acceptance checklist are in [SECURITY_AUDIT_0.31.0.md](SECURITY_AUDIT_0.31.0.md).
- **Executed this follow-up:** `./dev.sh test` passed every pre-Rust static/SQLite/security/JS/docs/translation check, then exited **127** on unavailable Cargo. `./dev.sh check` also exited **127**. `go test ./...` under `docker-agent/`, syntax checking all 27 browser JS files, OpenAPI JSON parse, shell syntax and separate source audits passed. A separate Chromium screenshot attempt timed out and does **not** count as verified UI rendering.
- **Rust compiler and runtime tests NOT RUN HERE**: `cargo`, `rustc` and `rustfmt` are absent; the only safe conclusion about E0716 is that its offending lifetime pattern was removed in source. Live BAN/PURGE over H1/H2/H3, TLS, reverse trust, performance and regression gates remain open.
## 0.31.0 negotiated panel content compression and HTTP BAN/PURGE follow-up (2026-10-10)
- `administration.panel_compression` defaults to `{zstd:true,br:true,gzip:true}` and uses the **existing Administration form/config revisions**. Authenticated panel HTML and embedded `/assets/` CSS/JS/language bundles are precompressed only once per resource; per-request browser `Accept-Encoding`/`q` selects zstd, br, gzip or identity. The public login HTML remains identity (static login assets can negotiate compression). `Vary`, `Content-Encoding`, `Content-Length` are set per response. Compression for public proxied responses is unchanged. An upstream reverse proxy can override `Accept-Encoding` and therefore the negotiated result.
- Empty-by-default `cache.http_invalidation_allowed_cidrs` restricts public-vhost `PURGE /path?query` exact removal and `BAN /prefix` lazy invalidation to allowed client IPv4/IPv6 ranges; `/0` rejected. Existing vhost/host ACL, rate limit and trusted forwarding logic remain in force. `BAN /` requires explicit confirmation; operation is audited, uses `spawn_blocking` around the existing cache engine and is never sent upstream. CIDR changes do not rotate cached objects.
- System information Refresh button is next to the API badge, without another row. EN/PL locales, OpenAPI and settings/cache/API docs were updated; SQLite schema remains 9 and Cold format remains 3.
- **PASS:** `./dev.sh test` completed all pre-Rust checks (SQLite migrations, release smoke, security, translations, Node UI, source contracts) then exited **127** because `cargo`/`rustc` are missing. `./dev.sh check` likewise exits **127**. Standalone `go test ./...` in `docker-agent/`, JavaScript syntax checks, OpenAPI JSON parse and Node form roundtrip passed. Chromium headless verified that API badge and System information Refresh button remain on the same row at viewport widths 320, 390, 1024 px.
- **Source-byte measurements (not wire captures)**: authenticated HTML after version substitution **320,756 B**; Zstd level 3 around **50,526 B**, Brotli level 5 around **43,654 B**, gzip level 6 around **47,343 B** using local utilities. The actual wire choice follows browser `q` values, not necessarily the smallest encoded representation. FCP/LCP and mobile real-browser startup were not measured on a running RustProxy.
- **Rust NOT RUN:** no Cargo/Rustc; Rust codec decoding, actual vhost BAN/PURGE authorization/HTTP/1-H2-H3, live proxy integration and exact wire metrics require a Rust-capable host. Production acceptance remains open.
## 0.31.0 mobile startup / HTML payload follow-up (2026-10-10)
- Removed `html.app-loading body { visibility: hidden }`. An initial CSS-driven overlay shows a loading spinner before the application code runs, a delayed (12 s) retry link when scripts or requests stall, and an explicit localized EN/PL failure state. No inline scripts or styles were introduced; authenticated CSP and the public login document are preserved. Reduced-motion preferences disable the spinner animation.
- Dashboard shell and live SSE start once the authenticated session and language bundle are resolved, **without blocking on** preferences, configuration, certificate inventory or initial `/api/status`. Unavailable configuration shows a visible toast and does not blank the page. This improves perceived startup but is not a measured FCP improvement.
- Moved the Overview **Refresh now** button into the title row, with no additional top-level row; the button remains hidden outside Overview.
- Private HTML is gzip-compressed and cached once in memory (no per-request recompression); `Accept-Encoding: gzip` yields `Content-Encoding: gzip`, `Vary: Accept-Encoding` and compressed `Content-Length`. Without gzip or with `gzip;q=0`, the existing identity HTML remains. Public login is not compressed/altered by this handler; API, SSE and proxy paths are not changed.
- **Measured from source bytes:** HTML source **318,905 B**, authenticated response after substituting two version placeholders **318,875 B** (previous packaged authenticated response **317,723 B**); Python gzip level 6 of the response **46,821 B** (about **85.3% less HTML transfer**). These are source-based estimates, **not HTTP wire captures from running RustProxy**, so browser-delivered Content-Length remains to be confirmed with a compiled server.
- **PASS:** deterministic Node startup/placement test, Chromium 320/390/1024 px rendering and title/button alignment, existing static checks and Node coordinator regressions. **NOT RUN:** Rust compilation or Rust tests (`cargo`/`rustc` unavailable), live HTTP gzip/identity/auth, real mobile hardware reproduction and field measurements (FCP/LCP/white-screen rate). Acceptance remains open.
## 0.31.0 dashboard refresh implementation and source verification (2026-10-10)
- Source delivery based on the supplied `RustProxy-0.30.1.zip` (no `.git`; remote `master` not verifiable). Implemented one per-browser scheduler, per-account/validated preferences, per-client SSE interval and fallback isolation, active-view Traffic/Cache, separated `/api/system-info`, compatible full `/api/status`, version/docs and English/Polish UI.
- **PASS:** source/version/module/security checks, SQLite schema-9/upgrade smoke, translations/docs, all bundled Node UI tests including deterministic refresh scheduler, Go agent unit tests, all admin JS syntax. Browser simulation checks 20 view transitions, no duplicated timers/in-flight requests, 0/manual, hidden tab, SSE failure/recovery, independent errors and logout.
- **NOT RUN:** Rust compilation, Rust tests and release build because `cargo`/`rustc`/`rustfmt` are missing (`./dev.sh test`/`check`/`fmt`/`release` exit 127 at toolchain gate). Real RustProxy API/SSE/traffic/regression tests and SSE bandwidth/CPU measurement remain unverified. The stress suite's Python HTTP test peer is **not** a RustProxy runtime test.
- See [full 0.31.0 implementation and verification report](IMPLEMENTATION_0.31.0.md). No database schema changes, git commit or tag.
## 0.30.1 sysinfo compiler-feature fix (2026-10-10)