Repository: traefik/traefik (language: go)

Issue:
It would be great if Traefik supported wasm plugins
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

Envoy WASM plugins：
https://github.com/envoyproxy/envoy/tree/master/examples/wasm-cc


Discussion / hints:
Hiya! Thanks for your suggestion.  We are interested in this issue but are unsure about the use case and the traction it will receive, so we are going to leave the status as "kind/proposal" to give the community time to let us know that they would like this.  

We will reevaluate as people respond.  

If you or another community member would like to build it before that happens, let us know and we will work with you to make sure you have all the information needed.  

Let us know here before you start. We prefer to work with our community members at the beginning of the design process to ensure that we are aligned and can move quickly with the review and merge process.  
I'd be really interested about this feature too. Right now creating plugins for multiple reverse proxies is a pain because of all the different API each reverse proxy propose.

[WasmProxy](https://github.com/proxy-wasm/spec) seems to be a very well fit usage.
I am glad this issue already exists on this repository. I have been working with Wasm, proxy-wasm and proxies' features for a long time and I really like to see a way to extend traefik through Wasm soon.

There are a few reasons why Wasm is a good match for this use case, the first one is the fact that currently using yaegi is the only way to extend Traefik with custom functionality, however it comes with some limitations:
1. it needs to catch up with Go language syntax on every release
2. is limited to Go
3. Panics
4. Libraries incompatibility
5. Lack of profiling tooling

As opposed Wasm present several advantages on these matters:
1. Polyglot
2. Sandboxed (hence no panics)
3. More mature compatibility matrix for different libraries
4. Profiling tooling
5. More mature ecosystem, broader community

that said, we have three alternatives to pursue this goal:
1. proxy-wasm: dead standard used in envoy, focused on envoy and with unexisting community. Still there are some cool projects built on top of it, although since the standard is mainly based in Envoy, some of the features won't be portable. (Spoiler alert, I worked a lot with proxy-wasm)
2. http-wasm: new alternative with a host available in Go that bring several improvements from the main issues with proxy-wasm (less envoy focused request/response lifecycle, works as middleware and proxy feature, more performant, etc).
3. custom ABI: this is an alternative but very laborous to implement as it would require us to write an own host, define an ABI and write a TCK for host and guests SDK. While this is possible I would rather not reinvent the wheel.

I am happy to contribute with a PR supporting http-wasm. Any thoughts on this?
Thanks for your feedback @jcchavezs, very much appreciated.
[http-wasm](https://http-wasm.io/) looks promising, but young, and from the author, not production ready:

> The current maturity phase is early draft

But it would make things a lot easier to integrate.
@jcchavezs how far have you been experimenting with http-wasm?
Any other known alternative?
At least I would like to see WASM support in Traefik. It could bring a lot more possibilities to plugin ecosystem (read: broader community and hopefully other products as well which will use WASM). If those 3 are the alternatives I would choose http-wasm and try to experiment that?
Some time ago, I attempted to integrate Proxy-WASM, and my experience left me somewhat dissatisfied. Regrettably, I couldn't achieve the seamless functionality of a proper Envoy plugin. It seemed like a substantial amount of effort would be necessary to address the missing components.

While I haven't explored HTTP-WASM, it appears to be a more attractive alternative. It seems relatively straightforward to integrate with Traefik. 
If someone has already developed a plugin for Envoy, such as CorazaWaf, they might need to create another plugin, but I'm not sure it's a real problem.

With the Yaegi plugin mechanism, you gain extensive access to the underlying Go objects, including the complete ResponseWriter. 
In theory, you could even hijack the connection, among other possibilities.
I'm curious if the HTTP-WASM "interface" will provide the required functionality for various middleware plugins. What kinds of middleware can be implemented with it? Perhaps we should consider implementing real middleware, similar to some of the existing Yaegi plugins, or even adapting the CorazaWaf middleware.



Hello,

I recently conducted some tests using `proxy-wasm`, but unfortunately, I encountered challenges in getting it to work effectively. I share the sentiment with you, @jcchavezs, that proxy-wasm is mainly based on Envoy. I'm not sure we will be able to have current Envoy Wasm filters working with Traefik.

On a more positive note, I found `http-wasm` to be a much simpler and more straightforward option, particularly when it comes to integration with Traefik. In fact, I even worked on a small project to explore its capabilities, which you can find [here](https://github.com/mmatur/http-wasm-traefik).

I'm in agreement with @juliens about implementing a real middleware with `http-wasm`. While it may not cover all the use cases that Yaegi currently handles, I view it as a promising initial step. Moreover, it can seamlessly coexist with plugin middleware.
@mmatur nice work! the actual change does not look even big! of course that is missing configurable parameters and kubernetes part etc. Anyway looks promising 
Really nice work @mmatur. I wonder if we could start a PR on this repo attempting to add http-wasm for traefik. There are a few things I want to discuss on this, for starting I would also suggest we look at existing impl. in dapr which also supports http-wasm (see https://github.com/dapr/docs/blob/v1.11/daprdocs/content/en/reference/components-reference/supported-middleware/middleware-wasm.md).

Some of the questions are:
- Do we support only local wasm files or also remote. If we don't support remote, are we expecting to use config map to pass the wasm binaries? Some binaries could take much longer than 1MB
- How do we pass config to the middlewares

@jcchavezs I'm curious if attempting to incorporate it into the existing plugin system might provide a solution. We've already established a mechanism for handling configuration, and for addressing the remote or local issue, this could potentially be a viable option. Although I'm uncertain about how straightforward it would be to integrate it into the current catalog system. Perhaps @ldez could share their perspective on this matter.
I can see solutions to integrate WASM plugins inside the catalog and Traefik (even if I still think that the best use of WASM is not for plugins).
It is useless to talk about that now, this is a detail that we will talk about when something will work locally inside Traefik (during the PR).

@jcchavezs,

I believe Traefik should support both local and remote Wasm files. Perhaps this support could be integrated into https://plugins.traefik.io for better accessibility.

Regarding the middleware configuration, I conducted some research and found that http-wasm-host-go offers a guestConfig option, which appears suitable for passing the config to the middleware. 

https://github.com/http-wasm/http-wasm-host-go/blob/4171a60674e821aa956c985fede9beb295d026c9/handler/options.go#L25-L30.

@ldez, could you please provide more details about your vision for the Wasm integration in Traefik?

> (even if I still think that the best use of WASM is not for plugins)

What are your ideas for utilizing Wasm in Traefik?
> What are your ideas for utilizing Wasm in Traefik?

This is off-topic: this topic is about WASM and plugins, we can talk about it somewhere else.

Maybe I was not clear: it's easy to integrate a different type of plugin if the repository follows the same convention as the existing plugins.
Some changes will be needed inside the remote plugin services and Traefik but, for now, based on the current information, I don't see any blockers.


Fix patch:
diff --git a/go.mod b/go.mod
index 7209fc0ab1..0089585ce9 100644
--- a/go.mod
+++ b/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/hashicorp/go-retryablehttp v0.7.4
 	github.com/hashicorp/go-version v1.6.0
 	github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f
+	github.com/http-wasm/http-wasm-host-go v0.5.2
 	github.com/influxdata/influxdb-client-go/v2 v2.7.0
 	github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
 	github.com/instana/go-sensor v1.38.3
@@ -62,6 +63,7 @@ require (
 	github.com/stretchr/testify v1.8.4
 	github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
 	github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
+	github.com/tetratelabs/wazero v1.5.0
 	github.com/traefik/grpc-web v0.16.0
 	github.com/traefik/paerser v0.2.0
 	github.com/traefik/yaegi v0.15.1
diff --git a/go.sum b/go.sum
index 543eb0b077..1f07133c45 100644
--- a/go.sum
+++ b/go.sum
@@ -1085,6 +1085,8 @@ github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09Uny
 github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
 github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/http-wasm/http-wasm-host-go v0.5.2 h1:5d/QgaaJtTF+qd0goBaxJJ7tcHP9n+gQUldJ7TsTexA=
+github.com/http-wasm/http-wasm-host-go v0.5.2/go.mod h1:zQB3w+df4hryDEqBorGyA1DwPJ86LfKIASNLFuj6CuI=
 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
 github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -1807,6 +1809,8 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz2
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ=
+github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0=
+github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
 github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
 github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
 github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
diff --git a/pkg/logs/wasm.go b/pkg/logs/wasm.go
new file mode 100644
index 0000000000..11d3e634f7
--- /dev/null
+++ b/pkg/logs/wasm.go
@@ -0,0 +1,32 @@
+package logs
+
+import (
+	"context"
+
+	"github.com/http-wasm/http-wasm-host-go/api"
+	"github.com/rs/zerolog"
+)
+
+// compile-time check to ensure ConsoleLogger implements api.Logger.
+var _ api.Logger = WasmLogger{}
+
+// WasmLogger is a convenience which writes anything above LogLevelInfo to os.Stdout.
+type WasmLogger struct {
+	logger *zerolog.Logger
+}
+
+func NewWasmLogger(logger *zerolog.Logger) *WasmLogger {
+	return &WasmLogger{
+		logger: logger,
+	}
+}
+
+// IsEnabled implements the same method as documented on api.Logger.
+func (w WasmLogger) IsEnabled(level api.LogLevel) bool {
+	return true
+}
+
+// Log implements the same method as documented on api.Logger.
+func (w WasmLogger) Log(_ context.Context, level api.LogLevel, message string) {
+	w.logger.WithLevel(zerolog.Level(level + 1)).Msg(message)
+}
diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go
index c2012427bf..42f1a5f059 100644
--- a/pkg/plugins/builder.go
+++ b/pkg/plugins/builder.go
@@ -4,28 +4,34 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
+	"path/filepath"
 
-	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/log"
-	"github.com/traefik/traefik/v3/pkg/logs"
-	"github.com/traefik/yaegi/interp"
-	"github.com/traefik/yaegi/stdlib"
 )
 
 // Constructor creates a plugin handler.
 type Constructor func(context.Context, http.Handler) (http.Handler, error)
 
+type pluginMiddleware interface {
+	NewHandler(ctx context.Context, next http.Handler) (http.Handler, error)
+}
+
+type middlewareBuilder interface {
+	newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error)
+}
+
 // Builder is a plugin builder.
 type Builder struct {
-	middlewareBuilders map[string]*middlewareBuilder
 	providerBuilders   map[string]providerBuilder
+	middlewareBuilders map[string]middlewareBuilder
 }
 
 // NewBuilder creates a new Builder.
 func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
+	ctx := context.Background()
+
 	pb := &Builder{
-		middlewareBuilders: map[string]*middlewareBuilder{},
+		middlewareBuilders: map[string]middlewareBuilder{},
 		providerBuilders:   map[string]providerBuilder{},
 	}
 
@@ -36,97 +42,142 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[
 			return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
 		}
 
-		logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
+		logger := log.With().
+			Str("plugin", "plugin-"+pName).
+			Str("module", desc.ModuleName).
+			Str("runtime", manifest.Runtime).
+			Logger()
+		logCtx := logger.WithContext(ctx)
 
-		i := interp.New(interp.Options{
-			GoPath: client.GoPath(),
-			Env:    os.Environ(),
-			Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
-			Stderr: logs.NoLevel(logger, zerolog.ErrorLevel),
-		})
+		switch manifest.Type {
+		case typeMiddleware:
+			middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName)
+			if err != nil {
+				return nil, err
+			}
 
-		err = i.Use(stdlib.Symbols)
-		if err != nil {
-			return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
-		}
+			pb.middlewareBuilders[pName] = middleware
 
-		err = i.Use(ppSymbols())
-		if err != nil {
-			return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
+		case typeProvider:
+			pBuilder, err := newProviderBuilder(logCtx, manifest, client.GoPath())
+			if err != nil {
+				return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
+			}
+
+			pb.providerBuilders[pName] = pBuilder
+
+		default:
+			return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
 		}
+	}
 
-		_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
+	for pName, desc := range localPlugins {
+		manifest, err := ReadManifest(localGoPath, desc.ModuleName)
 		if err != nil {
-			return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
+			return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
 		}
 
+		logger := log.With().
+			Str("plugin", "plugin-"+pName).
+			Str("module", desc.ModuleName).
+			Str("runtime", manifest.Runtime).
+			Logger()
+		logCtx := logger.WithContext(ctx)
+
 		switch manifest.Type {
-		case "middleware":
-			middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
+		case typeMiddleware:
+			middleware, err := newMiddlewareBuilder(logCtx, localGoPath, manifest, desc.ModuleName)
 			if err != nil {
 				return nil, err
 			}
 
 			pb.middlewareBuilders[pName] = middleware
-		case "provider":
-			pb.providerBuilders[pName] = providerBuilder{
-				interpreter: i,
-				Import:      manifest.Import,
-				BasePkg:     manifest.BasePkg,
+
+		case typeProvider:
+			builder, err := newProviderBuilder(logCtx, manifest, localGoPath)
+			if err != nil {
+				return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
 			}
+
+			pb.providerBuilders[pName] = builder
+
 		default:
 			return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
 		}
 	}
+	return pb, nil
+}
 
-	for pName, desc := range localPlugins {
-		manifest, err := ReadManifest(localGoPath, desc.ModuleName)
+// Build builds a middleware plugin.
+func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) {
+	if b.middlewareBuilders == nil {
+		return nil, fmt.Errorf("no plugin definitions in the static configuration: %s", pName)
+	}
+
+	// plugin (pName) can be located in yaegi or wasm middleware builders.
+	if descriptor, ok := b.middlewareBuilders[pName]; ok {
+		m, err := descriptor.newMiddleware(config, middlewareName)
 		if err != nil {
-			return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
+			return nil, err
 		}
 
-		logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
+		return m.NewHandler, nil
+	}
 
-		i := interp.New(interp.Options{
-			GoPath: localGoPath,
-			Env:    os.Environ(),
-			Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
-			Stderr: logs.NoLevel(logger, zerolog.ErrorLevel),
-		})
+	return nil, fmt.Errorf("unknown plugin type: %s", pName)
+}
 
-		err = i.Use(stdlib.Symbols)
+func newMiddlewareBuilder(ctx context.Context, goPath string, manifest *Manifest, moduleName string) (middlewareBuilder, error) {
+	switch manifest.Runtime {
+	case runtimeWasm:
+		wasmPath, err := getWasmPath(manifest)
 		if err != nil {
-			return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
+			return nil, fmt.Errorf("wasm path: %w", err)
 		}
 
-		err = i.Use(ppSymbols())
+		return newWasmMiddlewareBuilder(goPath, moduleName, wasmPath), nil
+
+	case runtimeYaegi, "":
+		i, err := newInterpreter(ctx, goPath, manifest.Import)
 		if err != nil {
-			return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
+			return nil, fmt.Errorf("failed to craete Yaegi intepreter: %w", err)
 		}
 
-		_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
+		return newYaegiMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
+
+	default:
+		return nil, fmt.Errorf("unknown plugin runtime: %s", manifest.Runtime)
+	}
+}
+
+func newProviderBuilder(ctx context.Context, manifest *Manifest, goPath string) (providerBuilder, error) {
+	switch manifest.Runtime {
+	case runtimeYaegi, "":
+		i, err := newInterpreter(ctx, goPath, manifest.Import)
 		if err != nil {
-			return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
+			return providerBuilder{}, err
 		}
 
-		switch manifest.Type {
-		case "middleware":
-			middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
-			if err != nil {
-				return nil, err
-			}
+		return providerBuilder{
+			interpreter: i,
+			Import:      manifest.Import,
+			BasePkg:     manifest.BasePkg,
+		}, nil
 
-			pb.middlewareBuilders[pName] = middleware
-		case "provider":
-			pb.providerBuilders[pName] = providerBuilder{
-				interpreter: i,
-				Import:      manifest.Import,
-				BasePkg:     manifest.BasePkg,
-			}
-		default:
-			return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
-		}
+	default:
+		return providerBuilder{}, fmt.Errorf("unknown plugin runtime: %s", manifest.Runtime)
 	}
+}
 
-	return pb, nil
+func getWasmPath(manifest *Manifest) (string, error) {
+	wasmPath := manifest.WasmPath
+	if wasmPath == "" {
+		wasmPath = "plugin.wasm"
+	}
+
+	if !filepath.IsLocal(wasmPath) {
+		return "", fmt.Errorf("wasmPath must be a local path")
+	}
+
+	return wasmPath, nil
 }
diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go
index 44a85672c6..52369585f0 100644
--- a/pkg/plugins/client.go
+++ b/pkg/plugins/client.go
@@ -279,7 +279,15 @@ func unzipFile(f *zipa.File, dest string) error {
 	defer func() { _ = rc.Close() }()
 
 	pathParts := strings.SplitN(f.Name, "/", 2)
-	p := filepath.Join(dest, pathParts[1])
+
+	var pp string
+	if len(pathParts) < 2 {
+		pp = pathParts[0]
+	} else {
+		pp = pathParts[1]
+	}
+
+	p := filepath.Join(dest, pp)
 
 	if f.FileInfo().IsDir() {
 		err = os.MkdirAll(p, f.Mode())
diff --git a/pkg/plugins/middlewarewasm.go b/pkg/plugins/middlewarewasm.go
new file mode 100644
index 0000000000..b307850d93
--- /dev/null
+++ b/pkg/plugins/middlewarewasm.go
@@ -0,0 +1,81 @@
+package plugins
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"path/filepath"
+	"reflect"
+
+	"github.com/http-wasm/http-wasm-host-go/handler"
+	wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp"
+	"github.com/tetratelabs/wazero"
+	"github.com/traefik/traefik/v3/pkg/logs"
+	"github.com/traefik/traefik/v3/pkg/middlewares"
+)
+
+type wasmMiddlewareBuilder struct {
+	path string
+}
+
+func newWasmMiddlewareBuilder(goPath string, moduleName, wasmPath string) *wasmMiddlewareBuilder {
+	return &wasmMiddlewareBuilder{path: filepath.Join(goPath, "src", moduleName, wasmPath)}
+}
+
+func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
+	return &WasmMiddleware{
+		middlewareName: middlewareName,
+		config:         reflect.ValueOf(config),
+		builder:        b,
+	}, nil
+}
+
+func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
+	code, err := os.ReadFile(b.path)
+	if err != nil {
+		return nil, fmt.Errorf("loading Wasm binary: %w", err)
+	}
+
+	logger := middlewares.GetLogger(ctx, middlewareName, "wasm")
+
+	opts := []handler.Option{
+		handler.ModuleConfig(wazero.NewModuleConfig().WithSysWalltime()),
+		handler.Logger(logs.NewWasmLogger(logger)),
+	}
+
+	i := cfg.Interface()
+	if i != nil {
+		config, ok := i.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("could not type assert config: %T", i)
+		}
+
+		data, err := json.Marshal(config)
+		if err != nil {
+			return nil, fmt.Errorf("marshaling config: %w", err)
+		}
+
+		opts = append(opts, handler.GuestConfig(data))
+	}
+
+	mw, err := wasm.NewMiddleware(context.Background(), code, opts...)
+	if err != nil {
+		return nil, err
+	}
+
+	return mw.NewHandler(ctx, next), nil
+}
+
+// WasmMiddleware is an HTTP handler plugin wrapper.
+type WasmMiddleware struct {
+	middlewareName string
+	config         reflect.Value
+	builder        wasmMiddlewareBuilder
+}
+
+// NewHandler creates a new HTTP handler.
+func (m WasmMiddleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
+	return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
+}
diff --git a/pkg/plugins/middlewares.go b/pkg/plugins/middlewareyaegi.go
similarity index 54%
rename from pkg/plugins/middlewares.go
rename to pkg/plugins/middlewareyaegi.go
index e46dbae344..452f8713b3 100644
--- a/pkg/plugins/middlewares.go
+++ b/pkg/plugins/middlewareyaegi.go
@@ -4,39 +4,25 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"os"
 	"path"
 	"reflect"
 	"strings"
 
 	"github.com/mitchellh/mapstructure"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"github.com/traefik/traefik/v3/pkg/logs"
 	"github.com/traefik/yaegi/interp"
+	"github.com/traefik/yaegi/stdlib"
 )
 
-// Build builds a middleware plugin.
-func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) {
-	if b.middlewareBuilders == nil {
-		return nil, fmt.Errorf("no plugin definition in the static configuration: %s", pName)
-	}
-
-	descriptor, ok := b.middlewareBuilders[pName]
-	if !ok {
-		return nil, fmt.Errorf("unknown plugin type: %s", pName)
-	}
-
-	m, err := newMiddleware(descriptor, config, middlewareName)
-	if err != nil {
-		return nil, err
-	}
-
-	return m.NewHandler, err
-}
-
-type middlewareBuilder struct {
+type yaegiMiddlewareBuilder struct {
 	fnNew          reflect.Value
 	fnCreateConfig reflect.Value
 }
 
-func newMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*middlewareBuilder, error) {
+func newYaegiMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*yaegiMiddlewareBuilder, error) {
 	if basePkg == "" {
 		basePkg = strings.ReplaceAll(path.Base(imp), "-", "_")
 	}
@@ -51,21 +37,35 @@ func newMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*middlewa
 		return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
 	}
 
-	return &middlewareBuilder{
+	return &yaegiMiddlewareBuilder{
 		fnNew:          fnNew,
 		fnCreateConfig: fnCreateConfig,
 	}, nil
 }
 
-func (p middlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
+func (b yaegiMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
+	vConfig, err := b.createConfig(config)
+	if err != nil {
+		return nil, err
+	}
+
+	return &YaegiMiddleware{
+		middlewareName: middlewareName,
+		config:         vConfig,
+		builder:        b,
+	}, nil
+}
+
+func (b yaegiMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
 	args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), cfg, reflect.ValueOf(middlewareName)}
-	results := p.fnNew.Call(args)
+	results := b.fnNew.Call(args)
 
 	if len(results) > 1 && results[1].Interface() != nil {
 		err, ok := results[1].Interface().(error)
 		if !ok {
 			return nil, fmt.Errorf("invalid error type: %T", results[0].Interface())
 		}
+
 		return nil, err
 	}
 
@@ -77,8 +77,8 @@ func (p middlewareBuilder) newHandler(ctx context.Context, next http.Handler, cf
 	return handler, nil
 }
 
-func (p middlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
-	results := p.fnCreateConfig.Call(nil)
+func (b yaegiMiddlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
+	results := b.fnCreateConfig.Call(nil)
 	if len(results) != 1 {
 		return reflect.Value{}, fmt.Errorf("invalid number of return for the CreateConfig function: %d", len(results))
 	}
@@ -107,27 +107,40 @@ func (p middlewareBuilder) createConfig(config map[string]interface{}) (reflect.
 	return vConfig, nil
 }
 
-// Middleware is an HTTP handler plugin wrapper.
-type Middleware struct {
+// YaegiMiddleware is an HTTP handler plugin wrapper.
+type YaegiMiddleware struct {
 	middlewareName string
 	config         reflect.Value
-	builder        *middlewareBuilder
+	builder        yaegiMiddlewareBuilder
+}
+
+// NewHandler creates a new HTTP handler.
+func (m *YaegiMiddleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
+	return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
 }
 
-func newMiddleware(builder *middlewareBuilder, config map[string]interface{}, middlewareName string) (*Middleware, error) {
-	vConfig, err := builder.createConfig(config)
+func newInterpreter(ctx context.Context, goPath string, manifestImport string) (*interp.Interpreter, error) {
+	i := interp.New(interp.Options{
+		GoPath: goPath,
+		Env:    os.Environ(),
+		Stdout: logs.NoLevel(*log.Ctx(ctx), zerolog.DebugLevel),
+		Stderr: logs.NoLevel(*log.Ctx(ctx), zerolog.ErrorLevel),
+	})
+
+	err := i.Use(stdlib.Symbols)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("failed to load symbols: %w", err)
 	}
 
-	return &Middleware{
-		middlewareName: middlewareName,
-		config:         vConfig,
-		builder:        builder,
-	}, nil
-}
+	err = i.Use(ppSymbols())
+	if err != nil {
+		return nil, fmt.Errorf("failed to load provider symbols: %w", err)
+	}
 
-// NewHandler creates a new HTTP handler.
-func (m *Middleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
-	return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
+	_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifestImport))
+	if err != nil {
+		return nil, fmt.Errorf("failed to import plugin code %q: %w", manifestImport, err)
+	}
+
+	return i, nil
 }
diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go
index 43beee8f03..367b6c46cf 100644
--- a/pkg/plugins/plugins.go
+++ b/pkg/plugins/plugins.go
@@ -138,18 +138,28 @@ func checkLocalPluginManifest(descriptor LocalDescriptor) error {
 	var errs *multierror.Error
 
 	switch m.Type {
-	case "middleware", "provider":
-		// noop
+	case typeMiddleware:
+		if m.Runtime != runtimeYaegi && m.Runtime != runtimeWasm && m.Runtime != "" {
+			errs = multierror.Append(errs, fmt.Errorf("%s: unsupported runtime '%q'", descriptor.ModuleName, m.Runtime))
+		}
+
+	case typeProvider:
+		if m.Runtime != runtimeYaegi && m.Runtime != "" {
+			errs = multierror.Append(errs, fmt.Errorf("%s: unsupported runtime '%q'", descriptor.ModuleName, m.Runtime))
+		}
+
 	default:
 		errs = multierror.Append(errs, fmt.Errorf("%s: unsupported type %q", descriptor.ModuleName, m.Type))
 	}
 
-	if m.Import == "" {
-		errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName))
-	}
+	if m.IsYaegiPlugin() {
+		if m.Import == "" {
+			errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName))
+		}
 
-	if !strings.HasPrefix(m.Import, descriptor.ModuleName) {
-		errs = multierror.Append(errs, fmt.Errorf("the import %q must be related to the module name %q", m.Import, descriptor.ModuleName))
+		if !strings.HasPrefix(m.Import, descriptor.ModuleName) {
+			errs = multierror.Append(errs, fmt.Errorf("the import %q must be related to the module name %q", m.Import, descriptor.ModuleName))
+		}
 	}
 
 	if m.DisplayName == "" {
diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go
index 78ea13a349..35b357faf6 100644
--- a/pkg/plugins/types.go
+++ b/pkg/plugins/types.go
@@ -1,5 +1,15 @@
 package plugins
 
+const (
+	runtimeYaegi = "yaegi"
+	runtimeWasm  = "wasm"
+)
+
+const (
+	typeMiddleware = "middleware"
+	typeProvider   = "provider"
+)
+
 // Descriptor The static part of a plugin configuration.
 type Descriptor struct {
 	// ModuleName (required)
@@ -19,9 +29,17 @@ type LocalDescriptor struct {
 type Manifest struct {
 	DisplayName   string                 `yaml:"displayName"`
 	Type          string                 `yaml:"type"`
+	Runtime       string                 `yaml:"runtime"`
+	WasmPath      string                 `yaml:"wasmPath"`
 	Import        string                 `yaml:"import"`
 	BasePkg       string                 `yaml:"basePkg"`
 	Compatibility string                 `yaml:"compatibility"`
 	Summary       string                 `yaml:"summary"`
 	TestData      map[string]interface{} `yaml:"testData"`
 }
+
+// IsYaegiPlugin returns true if the plugin is a Yaegi plugin.
+func (m *Manifest) IsYaegiPlugin() bool {
+	// defaults always Yaegi to have backwards compatibility to plugins without runtime
+	return m.Runtime == runtimeYaegi || m.Runtime == ""
+}


Test patch: