Skip to content

feat(axum): add safe diagnostics and trusted proxy metadata - #409

Open
ChristianPavilonis wants to merge 8 commits into
issue-393-buffered-body-limit-specfrom
issue-397-diagnostics-proxy-trust-spec
Open

ChristianPavilonis wants to merge 8 commits into
issue-393-buffered-body-limit-specfrom
issue-397-diagnostics-proxy-trust-spec

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Current review readiness — 2026-11-10

Ready for review at the publication head below. The required workspace and test aggregate have now passed in run42916832735; all 29 latest checks pass. Reviewers: aram356, prk-Jr, dhruv8sh.

Review transitions are independent of TS work and the combined integration audit; #462 remains draft. This current status supersedes historical draft-only prerequisites below without claiming unexecuted deployment/manual acceptance complete. All three reviewers are requested. Stack465 is the current grouping; #453 is excluded.

Current review-stack checkpoint

Publication head: 5bbff2bf6d6d853b470f76e7293b7ecfa55476f5. This cleanup preserves the existing feature unit and PR identity. The additional change is the completed generated-native build correction: image-only code now follows the scaffold's existing Fastly-example selection, with no request-policy change. Its focused generator/native proof is retained; fresh per-branch hosted checks are not assumed green.

Review order: #275 → #406 → #407 → #409 → #414 → #422 → #439 → #454. Draft integration checkpoint: #462. New SDK/group/codec/phase development and unrelated TS work are not prerequisites or additions to this PR. Earlier notes below retain their original tested revisions and limits; they are not a whole-tree pass for this new head.

Earlier implementation and proof notes

RC refresh checkpoint, 2026-11-10

Current head: 3b52b804e5570564cc95cdaaf4b9efc6307875ae.

Checked forwarding trusts the actual socket peer, validates the selected family and preserves original normalized-head accounting before stripping assertions and entering application callbacks. Diagnostics retain the existing terminal owner/time and redact raw values. Independent source review found no unresolved correctness defect.

The CLI-only diagnostics gate is fixed, with 57 CLI-only tests passing. Native ingress trust is now under edgezero_adapter_axum::trusted_proxy, leaving the current outbound legacy-API guard unchanged. Parent namespace proof passes 370 Axum unit, 28 contract and 32 production-hosting tests plus axum-only strict Clippy. Nested demo resolution matches existing exact workspace getrandom0.4.2/ipnet2.12.0 requirements; no manifest pin was relaxed.

This layer does not strip a future protected-CloudFront credential or prove a Geo reader identity.

Draft pending fresh hosted checks and the combined RC audit. Two unchanged native default-parallel deadline misses remain recorded and unclassified; serial passes and untouched-foundation parallel passes do not clear them. No timing bound was relaxed. No deployment, main merge, security-alert dismissal or review-thread resolution occurred. Historical design/review context follows.


Summary

  • Add opt-in, socket-peer-gated proxy trust and checked native ingress metadata. No proxies are trusted by default; managed admission strips both forwarding families.
  • Add safe correlated native records, request IDs, coherent work snapshots and an optional observer at the existing response terminal owner. Preserve application logging, completions, observers and custom rendering.
  • Use fixed native framework routing and JSON-deserialization error text, publish operator guidance, and add real proxy/TLS/socket regression coverage.

Changes

Crate / file Change
edgezero-adapter-axum Bounded literal policy/parser, owned metadata, strict settings, identity/counters/records, hosting integration and selected-status capture
edgezero-core Checked host witness, original normalized-head summary, timestamp-aware joined completion and bounded failure provenance; no report-shape or portable Tokio/Send change
Native fixtures Source-bound .2/.3 relays, verified test TLS with wrong-name control, 32 production-hosting cases, callback/panic/privacy/counter checks and defensive fallback tests
Docs Operator/migration guide, immutable reviewed specification and implementation/acceptance record

The approved JSON extension retains HTTP 400 and unmanaged serde detail. Custom renderers still own their response, but native framework JSON-deserialization errors now reach them as invalid JSON payload. Application-authored EdgeErrors are not globally sanitized. Managed raw-forwarding-header removal is documented as a compatibility change; the low-level converter remains a trust-policy bypass.

Dependency changes are four direct Axum edges to existing locked packages. Rustls and Tokio Rustls are dev-only. The published TLS private key is an explicitly public test asset, never a deployment credential.

Closes

Closes #397.

Stack and acceptance

Part of #391. This PR remains draft until the required integration and acceptance gates are satisfied.

Stacked above #407 using gh stack, at base 3308041b910b648af746c15e809ee86b96adc2e9. The user authorized implementation on this open stack. That exception does not satisfy accepted #275/#393 integration or authorize bypassing #394/#395 upload experiments. Accepted-stack, final-image and deployed-ingress acceptance remain deferred.

Test plan

Local checks passed for implementation commit 66a05e2:

  • cargo test --workspace --all-targets — 1,938 passed, one existing ignored generated-workspace compile smoke
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check and git diff --check
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • All three adapter WASM checks: Fastly wasm32-wasip1, Cloudflare wasm32-unknown-unknown, Spin wasm32-wasip2
  • cargo test -p edgezero-adapter-axum --all-targets --all-features — 255 library, 13 contract and 32 hosting cases
  • cargo test -p edgezero-core — 731 library tests plus integrations/doctests; 13 existing ignored doc examples
  • Locked docs install, lint, format and build; compiled guide API/options/snapshot smoke
  • Excluded examples/app-demo workspace
  • Final image and deployed proxy/provider acceptance

docs/superpowers/plans/2026-10-06-axum-diagnostics-trusted-proxy.md records commands, timings, historical failures, source fingerprints, all 22 specification rows and proof boundaries. Final runtime-input manifest SHA256: ac40934b17bb6c521951f15090e2accf3059abcb4d386c6da74a69d95de8c9d7.

Local wire tests directly prove owned proxy peers, consumer agreement, verified TLS, selected 200 plus failure after an observed head/prefix, hook/cardinality preservation and graceful/grace-forced work release. Missing-peer, exhaustive parser/identity bounds, precise injected timing and defensive fallback branches remain service/unit evidence. Environment-derived secret/config error privacy is tested; declared-store/backend and actual outbound-origin experiments are not claimed. Explicit diagnostic stop fixtures do not prove the convenience second-signal-plus-native-snapshot combination. The existing supported convenience second-signal test is retained.

Independent source reviews found and resolved lost Abort failure classification and formatting drift. A maintenance-only duplicated IPv6 bracket split remains unchanged; node and authority port grammars differ. Docs installation reports seven existing audit vulnerabilities, two moderate and five high; no docs package changes were made.

Checklist

  • Follows CLAUDE.md; shared Cargo/global formatting centrally scheduled
  • No portable Tokio dependencies or Send migration
  • Brace-style route parameters and core HTTP types
  • No exporters, public diagnostics endpoints, tracing propagation or new terminal owner
  • Application hook/logger/renderer ownership preserved
  • Test keys are explicitly public fixtures; no deployment secrets committed
  • Distinguish local proof from downstream acceptance; keep the PR draft until its merge gates are satisfied

@ChristianPavilonis
ChristianPavilonis force-pushed the issue-397-diagnostics-proxy-trust-spec branch from 66a05e2 to 9fa1666 Compare November 10, 2026 19:54

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

This PR adds peer-gated incoming-proxy trust, checked native ingress metadata, correlated native request records, work snapshots and an optional observer, plus fixed native routing/JSON error text. The lifecycle machinery is solid, but I'm requesting changes for:

  • a trust-walk behavior that lets an untrusted client collapse its identity to the trusted proxy's address;
  • a duplicate-Host gap in the new "checked" host;
  • a regression that drops the post-commit source-failure category from logs;
  • four new hosting tests that can't pass on macOS;
  • two open questions on how far the error redaction is meant to reach.

Findings

Blocking

  • 🔧 Untrusted client collapses to the trusted proxy's identity (crates/edgezero-adapter-axum/src/trusted_proxy.rs:417): a client-controlled malformed/oversized prefix invalidates the whole family, and effective_client becomes the proxy IP with source Direct.
  • 🔧 Duplicate Host → first value is the checked Direct host (crates/edgezero-adapter-axum/src/trusted_proxy.rs:369): inconsistent with inbound_origin.
  • 🔧 Post-commit body-source failure category no longer logged (crates/edgezero-adapter-axum/src/response.rs:516): NativeRequestRecord has no source-failure field.
  • 🔧 Four new proxy/TLS hosting tests always fail on macOS; the relay error is swallowed (crates/edgezero-adapter-axum/tests/fixtures/native_proxy.rs:268).
  • ❓ Path/query/form/Validated* errors still echo request input on the managed path (crates/edgezero-core/src/context.rs:666): intended exemption from the spec's safe-errors row?
  • ❓ Guide says unmanaged JSON errors keep detail; into_core_request redacts them (docs/guide/axum-diagnostics.md:427): which is intended?

Non-blocking (inline)

  • 🤔 Stripping rule too narrow (X_Forwarded_For, X-Real-IP, ...) and too broad (oauth2-proxy/Envoy X-Forwarded-* identity headers): trusted_proxy.rs:459
  • 🤔 New exact getrandom/ipnet pins downgrade the app-demo lock and pull in the wasip3 RC + a second wasm-tools tree: Cargo.toml:66
  • ♻️ #[non_exhaustive] on the public diagnostics/trust types: diagnostics.rs:197
  • ♻️ Public naming/surface (CheckedHostSource, from_ingress, CheckedProxyMetadata): ingress.rs:331
  • 🤔 Exposed request IDs reveal process request volume: diagnostics.rs:36
  • 🤔 Logger-panic isolation incomplete on the terminal path: response_egress.rs:1234
  • ♻️ New per-request route.clone() on every adapter: app.rs:348
  • 🤔 Sink/observer blocking scope understated: axum-diagnostics.md:434
  • ⛏ Duplicate/mislabeled identity-exhaustion logs and inconsistent event= format: diagnostics.rs:349
  • ⛏ strip_forwarding_headers is O(n²): trusted_proxy.rs:464
  • ⛏ failure=not_found ambiguity: diagnostics.rs:703
  • ⛏ 405 negative assertion can't fail: app.rs:2611
  • ⛏ Blank family vs blank CIDR asymmetry: trusted_proxy.rs:83
  • ⛏ Unsorted Cargo.lock entry: Cargo.lock:739
  • ⛏ IPv6 whole-family guard unreachable: trusted_proxy.rs:151
  • ⛏ Entropy failure cached forever: diagnostics.rs:273
  • ⛏ Process/stack language and stale facts in docs: axum-diagnostics.md:439

Non-blocking (not pinned to a diff line)

  • 🤔 Port race in production_hosting.rs grows, and this suite now runs in CI. address() (tests/production_hosting.rs:67-72) still binds 127.0.0.1:0, drops the listener, and hands the port to a child process. This PR raises the number of address() calls from 25 to 43, many inside loops. The 12 new failed() checks end with connect_timeout(addr).expect_err("failed startup never accepts connections") (line 249) on a port nothing holds, so a parallel test can be handed the same port and make that assertion fail spuriously. Measured locally: 1 of 3 parallel runs failed both_signals_stop_idle_processes with startup bind: listener: address unavailable; serial runs don't hit it. The harness runs this suite with --features axum,test-utils --all-targets --include-ignored (crates/edgezero-test-harness/src/runners/ci/platforms.rs:144-161), so the flake can turn CI red. Suggestion: keep a process-wide Mutex<HashSet<u16>> in address() so a port is never handed out twice, or retry spawn on that specific bind error, or run this binary with --test-threads=1.
  • 🤔 Test gaps behind documented claims.
    • The guide and adapters/axum.md say invalid settings fail startup in development and production, but nothing tests that capture_dev_environment (dev_server.rs:1054-1094) rejects invalid or non-Unicode TRUSTED_PROXY_CIDRS / FORWARDING_HEADER_FAMILY / LOGGING__REQUEST_RECORDS. The end-to-end invalid-settings tests only run "diagnostics" mode; loop them over ["diagnostics", "bare-dev"] like invalid_json_limits_fail_before_application_hooks_in_dev_and_production.
    • AxumDevServer::with_trusted_proxy_policy, with_request_records and with_diagnostics are untested, and AxumRunOptions::with_trusted_proxy_policy is only checked as a stored value.
    • In core, the FallbackBodyExceeded / FallbackReadTimedOut classification (router.rs:795-810), the pre-dispatch fixed_outcome branch, and the NormalizedIngressHeadSummary byte formula have no colocated tests.
    • The IPv6 branches in the proxy fixture (native_proxy.rs:179-182, 234-237) are unreachable because only IPv4 sockets are used, so IPv6 peers and quoted IPv6 nodes have unit coverage only.
  • ⛏ A backwards terminal instant still reaches terminal-time callbacks. In ResponseEgressAttempt::publish_report (response_egress.rs:1207-1236), when request_start <= observed_at < egress_started_at under an injected clock, the core report becomes Unspecified with zero elapsed, but complete_at(&report, observed_at) still forwards the instant, and native record() computes Some(duration). That contradicts the guide's "a backwards clock produces absent duration and a safe clock-fault category". Pass Option<MonotonicInstant> (None on regression), or have the native side check the outcome.
  • ⛏ Cancellation during zero-length stream validation is misrecorded. If the future is dropped during validate_empty_stream().await (response.rs:623-668), the attempt's Drop in the Initial state reports ConversionError. The native record then shows the selected status (e.g. 200) plus conversion_error although no response was sent, contradicting "no-response paths have no invented status". Report a cancellation disposition from Initial, or document the edge case.
  • ♻️ Simplifications.
    • The ConnectionScope check (diagnostics.rs:341-346) can't trigger in production, because ConnectionActivity is only minted by the same session; make it a debug_assert! and drop the variant.
    • set_failure(Option<_>) is only called with Some (service.rs:539-541), and prepare_egress_response(.., selected: Option<&SelectedStatus>) always gets Some in production.
    • set_ingress_error_kind (service.rs:523) is overwritten by the envelope's identical classification on the Response branch; it's only needed for Abort.
    • Every request allocates route.pattern().to_owned() and builds a record even when request records are off and there's no observer (diagnostics.rs:539-561); skip construction in that case, or store Arc<str> / Option<RouteMetadata>.
  • 🌱 The into_core_request bypass. Raw X-Forwarded-Host still drives ForwardedHost there (request.rs:44-56), so anyone embedding the low-level converter in their own Axum router is spoofable by default. Documented as a bypass, but inserting a no-trust normalize result (direct host only) would be cheap hardening.
  • 🌱 Pre-service rejections are invisible. Head-deadline failures (header_deadline.rs:198-201) and hyper parser 400/431 responses produce no record and no count. That matches the spec's parsed-service boundary, but the guide should tell operators.
  • 🌱 TLS fixture upkeep. tests/fixtures/tls/README.md has no regeneration commands, and since the CA key isn't committed (good), renewal means a new CA plus leaf. Nothing warns before the 2036-10-03 expiry; a -checkend-style assertion would. Consider ClientConfig::builder_with_provider(aws_lc_rs::default_provider().into()) in native_proxy.rs:284/302, so that feature unification enabling a second provider can't make builder() panic.

📌 Out of Scope

  • connection::tests::pending_chunked_forward_preserves_original_deadlines_over_tcp (connection.rs:1290, not touched by this PR) allows 5 ms on a 200 ms budget. It failed one parallel cargo test --workspace --all-targets run locally (handler terminal took 208.6ms, expected 200ms + at most 5 ms) and passed 10 of 10 isolated runs. Worth tracking separately alongside the "native default-parallel deadline misses" noted in the PR body.

CI Status

  • fmt: PASS
  • clippy (--workspace --all-targets --all-features -D warnings): PASS
  • tests (--workspace --all-targets): PASS on rerun (2,795 passed, 1 ignored); the first run hit the out-of-scope connection.rs timing flake above
  • cargo test -p edgezero-adapter-axum --all-targets --all-features (macOS): FAIL. 4 proxy/TLS tests fail deterministically (inline at native_proxy.rs:268), and 1 of 3 parallel runs also hit the port race
  • feature check (fastly cloudflare spin): PASS
  • spin wasm32-wasip2 check: PASS
  • hosted checks at 5bbff2b: 29/29 green

),
None => return metadata,
};
let Ok(parsed) = parse_result else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 An untrusted client can collapse its identity to the trusted proxy's address

The whole selected family is validated, including the client-written left prefix, before the trust walk. On Malformed (and on UnresolvedChain, lines 423-437) effective_client keeps the peer value from line 381 and client_source stays Direct.

Scenario, using the guide's own Cloudflare → LB topology: the visitor sends X-Forwarded-For: x. EdgeZero receives x, 203.0.113.7, 198.51.100.10 from the trusted LB 10.20.0.8. The family becomes Malformed, so effective_client() == 10.20.0.8 with source Direct. The same happens with 63+ entries (the 64-slot cap) or a client-added Forwarded element carrying proto=ftp or an invalid host.

Impact: the visitor evades per-client IP bans and rate limits keyed on effective_client, and passes any application allowlist that covers the proxy subnet. client_source() == Direct cannot distinguish "visitor connected directly" from "trusted proxy sent a chain we rejected". The guide's example (let client_boundary = metadata.effective_client();) checks neither source nor result. whole_family_validation_includes_unselected_prefix_and_by_nodes currently pins this behavior.

nginx real_ip_recursive avoids this by walking right to left and never parsing past the first untrusted hop, so a client-controlled prefix cannot invalidate the trusted suffix.

Fix (preferred): split each list into elements, walk right to left validating only trusted hops plus the boundary node, and ignore everything to the left. For Forwarded, keep the structural/quoting parse over the whole field (needed to find element boundaries), but do not let invalid values left of the boundary invalidate the assertion.

Fix (minimum): when the peer is trusted but no boundary is accepted, never report the proxy as the client:

// after the UntrustedPeer early return
metadata.effective_client = None;
metadata.client_source = CheckedHostSource::Unavailable;
// ...set them again only when the walk accepts a boundary

direct_peer() still exposes the proxy socket. Either way, the spec's "malformed whole-set fallback" row needs updating.

) -> CheckedProxyMetadata {
let direct_authority = match parts.uri.authority() {
Some(authority) => parse_authority(authority.as_str().as_bytes()).ok(),
None => parts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Duplicate Host headers: the first value becomes the checked Direct host

parts.headers.get("host") returns the first of several values. Neither hyper nor validate_normalized_head rejects duplicate Host. With Host: a.example followed by Host: b.example, effective_host() is a.example with source Direct, and ForwardedHost returns it. This also applies on trusted paths when the final Forwarded element has no host=. A front proxy that routes on the last or its own Host disagrees with the app. RFC 9112 §3.2 says such a request must be rejected with 400.

It also contradicts the existing origin witness: request.rs:133-137 (inbound_origin) already treats duplicate Host as unavailable.

Fix:

None => {
    let mut hosts = parts.headers.get_all(HOST).iter();
    match (hosts.next(), hosts.next()) {
        (Some(value), None) => parse_authority(value.as_bytes()).ok(),
        _ => None,
    }
}

Please add a unit test with two Host lines.

"response-egress Axum body source failed after commit: {}",
response_egress_source_error_category(&error)
);
Poll::Ready(Some(Err(_error))) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Post-commit body-source failure category is no longer logged anywhere

The base logged response-egress Axum body source failed after commit: {category} at WARN with the typed category (bad_gateway_transport, declared_length_exceeded, ...). This line deletes it on the grounds that "the native terminal owner records this once", but NativeRequestRecord has no source-failure field and record() (diagnostics.rs:524-552) never reads report.source_failure, which is populated at lines 488-490.

Scenario: an upstream stream fails mid-body. Before: a WARN naming the category. Now: outcome=source_error failure=none. With request_records=false, nothing at all, and observers can't see the cause either. The spec explicitly permits typed categories ("Log typed categories, not Display/Debug of error sources"), and ResponseEgressSourceFailure::category() is &'static str. The precommit zero-length path still logs its own WARN (line 656), so the "recorded once" rationale is applied inconsistently.

Fix: add a field before the record shape freezes:

pub source_failure: Option<&'static str>,
// in record():
source_failure: report.and_then(|r| r.source_failure).map(|f| f.category()),

and print source_failure={} in SafeRecord. Add an assertion to managed_source_and_deadline_failures_keep_selected_200_prefix_accounting_and_one_record.

{
let (head, body_prefix) = read_head(client).await?;
let rewritten = rewrite(&head, peer, assertions)?;
let socket = TcpSocket::new_v4()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 The four new proxy/TLS hosting tests always fail on macOS, and the cause is swallowed

relay() binds the outbound socket to 127.0.0.2/127.0.0.3. On Linux all of 127/8 is loopback, but macOS lo0 only has 127.0.0.1, so the bind fails with EADDRNOTAVAIL (verified: Can't assign requested address). let _relayed = relay(...) (lines 95 and 99) discards the error, the client reads an empty response, and the test panics with only response head or UnexpectedEof.

Affected: native_source_bound_single_proxy_and_mapped_cidr_match_only_owned_peers, native_two_proxy_chain_stops_at_untrusted_hop_and_gateway_replaces_prefix, native_selected_families_conflicts_malformed_sets_gaps_and_exhaustion, native_verified_tls_ingress_overwrites_public_assertions_and_rejects_wrong_name. The suite is gated on unix, so it runs on darwin. cargo test -p edgezero-adapter-axum --all-features fails 4/32 on every macOS run (5 serial and parallel runs locally), while Linux CI stays green.

Fix (any one), plus surfacing the relay error:

  • Probe-bind the source in LocalProxy::start and panic with an actionable message, e.g. needs loopback alias 127.0.0.2 (sudo ifconfig lo0 alias 127.0.0.2 up).
  • Gate these four tests on target_os = "linux".
  • At minimum, propagate the error out of the relay task (eprintln or channel) so the panic names the bind failure.

serde_json::from_slice(bytes.as_ref())
.map_err(|err| EdgeError::bad_request(format!("invalid JSON payload: {err}")))
serde_json::from_slice(bytes.as_ref()).map_err(|err| {
if self.body_policy.is_some() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Managed-native framework errors still echo request input outside JSON

Only json_within got the fixed-text treatment. On the same managed native path these framework call sites still put raw input into default public errors:

  • context.rs:599: invalid form payload: {err}
  • context.rs:825: invalid path parameters: {err}, e.g. /items/PATH_SENTINEL → invalid type: string "PATH_SENTINEL", expected u32
  • context.rs:854: invalid query string: {err} (e.g. unknown variant `X` )
  • extractor.rs:82,100,331,407,483,524: Validated* → EdgeError::validation(err.to_string()). validator 0.20+ includes the rejected value in params, so {"token":"PAYLOAD_SENTINEL"} renders ... "value": String("PAYLOAD_SENTINEL").

wire_message returns the message verbatim for BadRequest/Validation. The spec says "Framework call sites must use safe fixed messages where they create those categories", and its "Safe errors and records" acceptance row says "Also test query/path parameters ... and payloads". The current tests cover a route-path sentinel and a Json<u64> sentinel only.

Is this an intentional exemption? If so, please record it in the spec and guide. If not, apply the same managed gate with fixed text (invalid path parameters, invalid query string, invalid form payload, validation failed) and add sentinel tests for each.

cidrs: Option<&str>,
family: Option<&str>,
) -> Result<Self, ProxyTrustConfigError> {
let selected_family = family

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ An empty family setting fails startup, but an empty CIDR list means "no trust"

EDGEZERO__ADAPTER__FORWARDING_HEADER_FAMILY= (set but blank) arrives as Some(""), trims to "", and is rejected, while EDGEZERO__ADAPTER__TRUSTED_PROXY_CIDRS= is accepted as no trust. Templated deployments (Helm, compose, systemd env files) commonly render unused variables as empty, so the same template fails for one variable and not the other.

Suggestion: treat a blank family as unset (and still reject a nonempty CIDR list without a family), or document the asymmetry in the settings table.

Comment thread Cargo.lock
"httparse",
"hyper",
"hyper-util",
"libc",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ The committed lock has an unsorted dependency list

"libc", "ipnet" is out of order. Any cargo invocation without --locked rewrites the file; a plain local cargo test here immediately showed M Cargo.lock with exactly this swap. CI uses --locked, which compares semantically, so it passes there. Please commit cargo's normalized output.

normalized.push(network);
}
}
if covers_family(&normalized, false) || covers_family(&normalized, true) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ The IPv6 "whole family" guard can never fire

Every IPv6 network with prefix < 96 that contains ::ffff:0:0 is rejected by the mapped-range rule, and every /96+ mapped network is projected to IPv4 (lines 305-310). So the normalized IPv6 set can never cover ::ffff:0:0, and covers_family(&normalized, true) is always false. ::/1 is rejected only by the mapped rule. Meanwhile 8000::/1,4000::/2,2000::/3 (all routable unicast IPv6, i.e. effectively trust-all for real clients) is accepted.

Suggestion: either drop the branch and document that the trust-all guard is best-effort, or define "trust-all" for IPv6 as covering 2000::/3 and test the combined-coverage case.

requests: AtomicU64,
}

static PROCESS_NONCE: OnceLock<Result<[u8; 16], IdentityFault>> = OnceLock::new();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ One entropy failure poisons every later runner in the process

PROCESS_NONCE: OnceLock<Result<..>> caches Err permanently. A single transient getrandom failure at the first NativeSession::new makes every later session in the process fail, including in-process restarts and embedded hosts.

Suggestion: cache only Ok (retry on Err), or document that an entropy failure is process-fatal for native hosting.

native accounting, but panic-abort, SIGKILL, and process failure have no completion or log-flush
promise. There is no retry, durable delivery guarantee, or added shutdown flush budget.

Deployment acceptance remains downstream. Parser tests and local socket/TLS fixtures do not

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Process and stack language in user-facing and committed docs

  • "Recheck the accepted prerequisite stack ..." (439-443) means nothing to a guide reader. Rewrite it as plain operator guidance: validate the deployed image and proxy configuration before relying on these properties.
  • The plan record (docs/superpowers/plans/2026-10-06-axum-diagnostics-trusted-proxy.md) has stale facts: rustls 0.23.40 (lines 291/365/375; the workspace pins =0.23.46), Rust 1.95.0 (347/375; .tool-versions is 1.98.1), "No package version changed" (375; see the app-demo lock comment), unchecked gates (18-20, 227-229), and evidence under /tmp/edgezero-impl-397/. Should it be refreshed, or explicitly marked as a frozen historical record?
  • The spec (line 12) commits a personal worktree path, /home/pav/worktrees/....
  • The moved comment Per spec 6.3.1. at crates/edgezero-core/src/error.rs:406 is a process reference; describe the constraint instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Axum: add safe production diagnostics and trusted-proxy handling

2 participants