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:

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.