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)
- 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. - Digest format check —
source.digestmust be a 64-char SHA-256 hex string (case-insensitive; normalized lowercase). - Cache lookup — the artifact is content-addressed:
<cache_dir>/<digest>.wasm(source.cache_pathoverrides 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. - 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.
- Digest verification — downloaded bytes must hash to the pinned digest; mismatch fails closed naming both digests.
- Signature verification — when
signature/public_key(or pinnedplugin_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 apublic_keywithout asignatureis rejected as a config mistake (a key that verifies nothing). - 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] / aDownloadFailednaming 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§
- Source
Error - Why a
source:plugin could not be resolved. Every variant names the plugin and the exact pipeline step that failed;Displaymessages 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_diris absent (relative to the gateway working directory, the same posture as the ACMEstate_dirdefault). - 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 intests/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. Localwasm:/nativeplugins 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 theRESOLUTION_BUDGETper-publish budget.
Type Aliases§
- Source
Resolutions - Per-plugin resolution results keyed by plugin name (only
source:plugins appear). Built byresolve_sources; consumed byPluginLifecycle::load.