http-wasm-guest: Rust-Guest für Traefik-Wasm-Plugins
Rust-Guest-Bibliothek für Traefik-Wasm-Middlewares
celguard läuft als WebAssembly-Plugin in Traefik. Die eigentliche Regelauswertung ist dabei nur die obere Schicht – darunter liegt http-wasm-guest, eine schlanke Rust-Bibliothek, die das WebAssembly-Guest-ABI für http-wasm abbildet und damit erst möglich macht, ein Traefik-Middleware komplett in Rust zu schreiben.
Wasm-Middleware bei Traefik
Traefik kann Middlewares als WebAssembly-Module laden. Das Modell ist das klassische Host-/Guest-Schema von WebAssembly: Der Host ist der laufende Prozess – hier der Traefik-Server –, in den eine Wasm-Runtime eingebettet ist, und der Gast ist ein Programm, das ins Wasm-Binärformat kompiliert wurde. Die http-wasm-ABI bildet den Vertrag zwischen beiden Seiten ab – vor allem die Funktionen, die jeder Seite zur Verfügung stehen.
Da WebAssembly eine sandbox-ähnliche Architektur ist, kann der Host-Prozess extern definierten Code sicher ausführen – gerade deshalb eignet es sich für Plugins wie celguard.
Das Guest-ABI in Rust
http-wasm-guest ist bewusst dünn gehalten: Es kapselt die Host-Funktionen, gibt aber keine eigene Sicht auf HTTP vor. Ein Plugin implementiert das Guest-Trait und registriert sich einmal bei der Runtime:
use http_wasm_guest::{
Guest,
host::{Request, Response},
register,
};
struct Plugin {}
impl Guest for Plugin {
fn handle_request(&self, request: &Request, _response: &Response) -> (bool, i32) {
request.header.add(b"X-Custom-Header", b"FooBar");
(true, 0) // (true, ctx) = weiterschalten; (false, _) = kurzschließen
}
}
fn main() {
register(Plugin {});
}
handle_request läuft vor der Weiterleitung an den Upstream und handle_response nach dessen Antwort. Das Tupel-Ergebnis steuert, ob die Anfrage fortgesetzt wird, und übergibt optional einen ctx-Wert in den Response-Hook. Die Exporte, die das ABI verlangt, erzeugt die Bibliothek dabei selbst – man rührt nicht an FFI-Details.
Minimal und speicherbewusst
Ziel ist eine geringe Abhängigkeitsfläche und ein sparsamer Umgang mit dem Speicher, da der Gast in einer eingebetteten Runtime läuft:
- Alle Byte-Daten werden als
bytes::Bytesausgetauscht – zero-copy und günstig zu klonen. - Um Heap-Allokationen auf den heißen Pfaden (Logging, Lesen vom Host) zu vermeiden, werden Puffer vorallokiert und wiederverwendet.
- Bei großen Payloads greift ein Overflow-Pfad, der sich den nötigen Speicher auf dem Heap allokiert; die Puffergröße ist auf 16 MB gedeckelt, darüber wird abgeschnitten.
- Log-Nachrichten laufen über einen festen 2048-Byte-Buffert; längere werden gekürzt.
Die einzige Pflicht-Abhängigkeit im Runtime ist bytes; die log-Integration lässt sich deaktivieren. Die Korrektheit der FFI-Schicht wird in CI unter anderem mit Miri geprüft – dabei wurde unter anderem ein Speicherleck im Overflow-Pfad für große Header gefunden und behoben.
celguard baut darauf auf
celguard ist genau so ein Gast. Der Plugin-Kern ist bewusst klein: Er implementiert das Guest-Trait, liest seine Konfiguration über die Host-Schnittstelle und leitet die eigentliche Arbeit an einen CEL-basierten Matcher weiter. Die FFI-, Puffer- und Speicherfragen trägt http-wasm-guest komplett; in celguard bleibt nur:
impl<'a> Guest for Plugin<'a> {
fn handle_request(&self, request: &Request, response: &Response) -> (bool, i32) {
match self.matcher.evaluate(request) {
Ok(Outcome::Match(action)) => self.execute(action, response),
Ok(Outcome::NoMatch) => (true, 0),
Err(err) => {
log::error!("Matcher: {}", err);
(true, 0)
}
}
}
}
Damit bleibt der celguard-Code lesbar und auf die Regellogik fokussiert – die Arbeit an der Wasm-Grenzschichte erledigt http-wasm-guest im Hintergrund.