Module source

Module source 

Source
Expand description

Registry plugin source resolution (DW-165).

A plugin entry with source: names a remote .wasm artifact (registry URL + mandatory SHA-256 digest + optional Ed25519 signature/key material). This module resolves every such entry to a VERIFIED local file BEFORE the wasm load step (PluginLifecycle:: load), so a registry plugin enters the runtime through exactly the same read/compile/health path as a local wasm: plugin — there is no partial load: an artifact that cannot be resolved or fails verification never reaches wasmtime, and the plugin is marked Crashed at publish (fail-closed for its routes, DW-157/158 vocabulary).

§Resolution pipeline (per source: plugin)

  1. Scheme check — only https:// is fetchable. oci:// references parse in config but are rejected with a clear validation error in this version (see the registry spec in the user docs); the resolver repeats the rejection defensively.
  2. Digest format check — source.digest must be a 64-char SHA-256 hex string (case-insensitive; normalized lowercase).
  3. Cache lookup — the artifact is content-addressed: <cache_dir>/<digest>.wasm (source.cache_path overrides the location, not the verification; it must be a relative path with no .. segments — containment, see [PluginSourceConfig:: cache_path_violation]). A present cache file whose bytes do not hash to the pinned digest is a tampered/corrupt entry and FAILS CLOSED with a digest-mismatch error naming the expected and actual digests — it is never silently re-fetched (an operator must see that the file named for one digest holds another). A cache hit still verifies the signature.
  4. Download (cache miss only, and only while the per-publish budget has time left) — one HTTPS GET, HTTP/1.1, redirects NOT followed, response size capped. See the downloader note below for why this is a hand-rolled client.
  5. Digest verification — downloaded bytes must hash to the pinned digest; mismatch fails closed naming both digests.
  6. Signature verification — when signature/public_key (or pinned plugin_registry.public_keys) are configured, an Ed25519 signature over the artifact bytes must verify. A plugin with no signature is REJECTED when the registry pins keys, and a public_key without a signature is rejected as a config mistake (a key that verifies nothing).
  7. Atomic cache write — tmp file in the same directory, fsync, rename into place. A crash mid-write can never leave a partial artifact under the digest name; the next resolve either sees the complete file or downloads again.

§Downloader (why a hand-rolled client)

dwara-core has no reqwest/ureq; the repo precedent for outbound HTTP is the events domain’s hand-rolled HTTP/1.1 client (events::webhook, tokio-based). That client is private to events, which the wasm domain must not import (check_deps.py: wasm <- config, plugins), and it is async while resolution runs inside the SYNCHRONOUS DataPlane::reload_plugins (called from async reload loops — a nested block_on would panic). So this module carries its own minimal SYNCHRONOUS HTTPS GET: std TcpStream + rustls ClientConnection (both already direct dependencies), public webpki roots, the same egress posture as webhooks (operator-configured URL, SSRF egress filter applied at connect time when enabled, redirects not followed).

§Time budget (review finding: no unbounded holds)

Two layers bound every resolution, because a registry that drips one byte per read would otherwise defeat a per-read timeout (each read resets it) and hold a thread forever:

  • a per-READ timeout (10s connect, 30s per read), and
  • a per-PUBLISH total budget (RESOLUTION_BUDGET, 60s across ALL source plugins of one publish), checked before each download and inside the fetcher’s read loop. A cache hit needs no network and is never budget-gated. Budget exhaustion fails the affected plugin closed with a step-named error ([SourceError:: ResolutionBudgetExceeded] / a DownloadFailed naming the budget), never a hang.

The binary’s reload path additionally runs the whole resolution on tokio::task::spawn_blocking (DNS via getaddrinfo and the sync TLS reads are blocking syscalls), so the async reload task awaits instead of holding a worker thread; see dwara-bin’s reload driver.

§Cache layout

<cache_dir>/                 (plugin_registry.cache_dir, default ./plugin-cache)
  <sha256-digest>.wasm       one immutable artifact per digest
  .<sha256-digest>.wasm.tmp<pid>   transient write target (never read)

Artifacts are content-addressed: two plugins pinning the same digest share one file, and a restart (or a second gateway process) re-resolves from the cache without network access — the restart-safety test pins this by pointing url at a dead port and resolving from cache alone.

Enums§

SourceError
Why a source: plugin could not be resolved. Every variant names the plugin and the exact pipeline step that failed; Display messages are operator-facing (they reach the crashed-plugin error string on the status surface and the publish-time log line).

Constants§

DEFAULT_CACHE_DIR
Default cache directory when plugin_registry.cache_dir is absent (relative to the gateway working directory, the same posture as the ACME state_dir default).
MAX_ARTIFACT_BYTES
Hard cap on one downloaded artifact (bytes). proxy-wasm filter modules are small; this binds a hostile registry answer.
RESOLUTION_BUDGET
Total wall-clock budget for registry-source resolution per publish, shared across ALL source: plugins (review finding: a drip-feeding or hung registry must not hold the reload path unboundedly, and N plugins must not stack N budgets). Checked before each download and between reads in the fetch loop; cache hits are never budget-gated (they need no network). Exhaustion fails the affected plugin closed with a step-named error; the next publish retries.

Functions§

decode_chunked
Minimal chunked transfer decoding (hex size; optional chunk extensions after ; ignored; final 0-chunk ends the body, trailers after it are accepted and dropped). Public for the integration tests in tests/wasm_plugin_source.rs, which pin the hostile framings (overflow size lines, over-cap sizes, malformed hex, missing CRLFs) as pure functions.
parse_response
Parse a raw HTTP/1.1 response: status line, headers, body framing (Content-Length, chunked, or read-to-EOF). Only 200 is accepted; redirects are deliberately not followed (a registry behind a redirect must be configured at its final URL — the same posture as webhook deliveries). Public for the integration tests in tests/wasm_plugin_source.rs, which pin the framing edge cases (truncation, caps, hostile chunk sizes) as pure functions.
resolve_sources
Resolve every source: plugin in the gateway config to a verified local artifact path. Local wasm:/native plugins are absent from the map. Successes are logged (cache vs download); failures are logged by the lifecycle when they mark the plugin Crashed (this function stays quiet on errors so the crash log line is the single publish-time record). The whole resolution runs under the RESOLUTION_BUDGET per-publish budget.

Type Aliases§

SourceResolutions
Per-plugin resolution results keyed by plugin name (only source: plugins appear). Built by resolve_sources; consumed by PluginLifecycle::load.