2023-04-06 21:12:07 +00:00
// Copyright (c) HashiCorp, Inc.
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
2023-04-06 21:12:07 +00:00
package wasm
import (
"fmt"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
2023-05-30 14:17:33 +00:00
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
2023-04-06 21:12:07 +00:00
"github.com/hashicorp/consul/api"
2023-05-30 14:17:33 +00:00
cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
2023-04-06 21:12:07 +00:00
)
// wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins.
type wasm struct {
2023-05-30 14:17:33 +00:00
cmn . BasicExtensionAdapter
2023-05-26 18:10:31 +00:00
2023-04-06 21:12:07 +00:00
name string
wasmConfig * wasmConfig
}
var supportedRuntimes = [ ] string { "v8" , "wamr" , "wavm" , "wasmtime" }
2023-05-30 14:17:33 +00:00
var _ cmn . BasicExtension = ( * wasm ) ( nil )
2023-04-06 21:12:07 +00:00
2023-05-30 14:17:33 +00:00
func Constructor ( ext api . EnvoyExtension ) ( cmn . EnvoyExtender , error ) {
2023-04-06 21:12:07 +00:00
w , err := construct ( ext )
if err != nil {
return nil , err
}
2023-05-30 14:17:33 +00:00
return & cmn . BasicEnvoyExtender {
2023-04-06 21:12:07 +00:00
Extension : & w ,
} , nil
}
func construct ( ext api . EnvoyExtension ) ( wasm , error ) {
w := wasm { name : ext . Name }
if w . name != api . BuiltinWasmExtension {
return w , fmt . Errorf ( "expected extension name %q but got %q" , api . BuiltinWasmExtension , w . name )
}
if err := w . fromArguments ( ext . Arguments ) ; err != nil {
return w , err
}
// Configure the failure behavior for the filter. If the plugin is required,
// then filter runtime errors result in a failed request (fail "closed").
// Otherwise, runtime errors result in the filter being skipped (fail "open").
w . wasmConfig . PluginConfig . failOpen = ! ext . Required
return w , nil
}
func ( w * wasm ) fromArguments ( args map [ string ] any ) error {
var err error
w . wasmConfig , err = newWasmConfig ( args )
if err != nil {
return fmt . Errorf ( "error decoding extension arguments: %w" , err )
}
return w . wasmConfig . validate ( )
}
// CanApply indicates if the WASM extension can be applied to the given extension configuration.
2023-05-30 14:17:33 +00:00
func ( w wasm ) CanApply ( cfg * cmn . RuntimeConfig ) bool {
return cfg . Kind == w . wasmConfig . ProxyType &&
cfg . Protocol == w . wasmConfig . Protocol
2023-05-23 11:55:06 +00:00
}
2023-04-06 21:12:07 +00:00
2023-05-23 11:55:06 +00:00
func ( w wasm ) matchesConfigDirection ( isInboundListener bool ) bool {
2023-05-30 14:17:33 +00:00
return ( isInboundListener && w . wasmConfig . ListenerType == "inbound" ) ||
( ! isInboundListener && w . wasmConfig . ListenerType == "outbound" )
2023-04-06 21:12:07 +00:00
}
2023-05-30 14:17:33 +00:00
// PatchFilters adds a Wasm HTTP or TCP filter to the filter chain.
func ( w wasm ) PatchFilters ( cfg * cmn . RuntimeConfig , filters [ ] * envoy_listener_v3 . Filter , isInboundListener bool ) ( [ ] * envoy_listener_v3 . Filter , error ) {
2023-05-23 11:55:06 +00:00
if ! w . matchesConfigDirection ( isInboundListener ) {
2023-05-30 14:17:33 +00:00
return filters , nil
2023-05-23 11:55:06 +00:00
}
2023-05-30 14:17:33 +00:00
// Check that the Wasm plugin protocol matches the service protocol.
// It is a runtime error if the extension is configured to apply a Wasm plugin
// that doesn't match the service's protocol.
// This shouldn't happen because the caller should check CanApply first, but just in case.
if cfg . Protocol != w . wasmConfig . Protocol {
return filters , fmt . Errorf ( "failed to apply Wasm filter: service protocol for %q is %q but protocol for the Wasm extension is %q. Please ensure the protocols match" ,
cfg . ServiceName . Name , cfg . Protocol , w . wasmConfig . Protocol )
2023-04-06 21:12:07 +00:00
}
2023-05-30 14:17:33 +00:00
// Generate the Wasm plugin configuration. It is the same config for HTTP and network filters.
2023-04-06 21:12:07 +00:00
wasmPluginConfig , err := w . wasmConfig . PluginConfig . envoyPluginConfig ( cfg )
if err != nil {
2023-05-30 14:17:33 +00:00
return filters , fmt . Errorf ( "failed to encode Envoy Wasm plugin configuration: %w" , err )
2023-04-06 21:12:07 +00:00
}
2023-05-30 14:17:33 +00:00
// Insert the filter immediately before the terminal filter.
insertOptions := cmn . InsertOptions { Location : cmn . InsertBeforeFirstMatch }
2023-04-06 21:12:07 +00:00
2023-05-30 14:17:33 +00:00
switch cfg . Protocol {
case "grpc" , "http2" , "http" :
insertOptions . FilterName = "envoy.filters.http.router"
filter , err := cmn . MakeEnvoyHTTPFilter ( "envoy.filters.http.wasm" , & envoy_http_wasm_v3 . Wasm { Config : wasmPluginConfig } )
if err != nil {
return filters , fmt . Errorf ( "failed to make Wasm HTTP filter: %w" , err )
2023-04-06 21:12:07 +00:00
}
2023-05-30 14:17:33 +00:00
return cmn . InsertHTTPFilter ( filters , filter , insertOptions )
case "tcp" :
fallthrough
default :
insertOptions . FilterName = "envoy.filters.network.tcp_proxy"
filter , err := cmn . MakeFilter ( "envoy.filters.network.wasm" , & envoy_wasm_v3 . Wasm { Config : wasmPluginConfig } )
if err != nil {
return filters , fmt . Errorf ( "failed to make Wasm network filter: %w" , err )
}
return cmn . InsertNetworkFilter ( filters , filter , insertOptions )
2023-04-06 21:12:07 +00:00
}
}