From 22e1afc984b295b83a2ec4b1bf4e1bc22c841016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 3 Jul 2024 10:24:17 +0800 Subject: [PATCH] add listener filter support in native plugin (#621) Signed-off-by: spacewander --- api/pkg/plugins/plugins.go | 4 +- controller/internal/istio/envoyfilter.go | 139 ++++++++++++------ controller/internal/model/model.go | 7 + .../internal/translation/final_state.go | 4 - .../internal/translation/merged_state.go | 57 +++++-- .../translation/l4_native_plugin_only.in.yml | 33 +++++ .../translation/l4_native_plugin_only.out.yml | 68 +++++++++ .../internal/translation/translation.go | 5 + controller/plugins/plugins.go | 1 + .../plugins/testdata/network/default.yml | 14 ++ .../testdata/network/tls_inspector.in.yml | 13 ++ .../testdata/network/tls_inspector.out.yml | 27 ++++ controller/plugins/tlsinspector/config.go | 32 ++++ controller/plugins/translation_test.go | 118 +++++++++++++++ e2e/tests/tls_inspector.go | 33 +++++ e2e/tests/tls_inspector.yml | 30 ++++ .../docs/reference/plugins/tls_inspector.md | 53 +++++++ .../docs/reference/plugins/tls_inspector.md | 53 +++++++ types/plugins/plugins.go | 1 + types/plugins/tlsinspector/config.go | 44 ++++++ 20 files changed, 675 insertions(+), 61 deletions(-) create mode 100644 controller/internal/translation/testdata/translation/l4_native_plugin_only.in.yml create mode 100644 controller/internal/translation/testdata/translation/l4_native_plugin_only.out.yml create mode 100644 controller/plugins/testdata/network/default.yml create mode 100644 controller/plugins/testdata/network/tls_inspector.in.yml create mode 100644 controller/plugins/testdata/network/tls_inspector.out.yml create mode 100644 controller/plugins/tlsinspector/config.go create mode 100644 controller/plugins/translation_test.go create mode 100644 e2e/tests/tls_inspector.go create mode 100644 e2e/tests/tls_inspector.yml create mode 100644 site/content/en/docs/reference/plugins/tls_inspector.md create mode 100644 site/content/zh-hans/docs/reference/plugins/tls_inspector.md create mode 100644 types/plugins/tlsinspector/config.go diff --git a/api/pkg/plugins/plugins.go b/api/pkg/plugins/plugins.go index 3a08a8cd..1d3f0156 100644 --- a/api/pkg/plugins/plugins.go +++ b/api/pkg/plugins/plugins.go @@ -99,7 +99,9 @@ func RegisterPlugin(name string, plugin Plugin) { goPlugin.Factory(), NewPluginConfigParser(goPlugin)) } else if _, ok := plugin.(NativePlugin); ok { - if order.Position != OrderPositionOuter && order.Position != OrderPositionInner { + switch order.Position { + case OrderPositionOuter, OrderPositionInner, OrderPositionListener, OrderPositionNetwork: + default: panic(errInvalidNativePluginOrder) } } else { diff --git a/controller/internal/istio/envoyfilter.go b/controller/internal/istio/envoyfilter.go index 1ecdc74e..bbd314e2 100644 --- a/controller/internal/istio/envoyfilter.go +++ b/controller/internal/istio/envoyfilter.go @@ -23,6 +23,7 @@ import ( istiov1a3 "istio.io/client-go/pkg/apis/networking/v1alpha3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fmModel "mosn.io/htnn/api/pkg/filtermanager/model" "mosn.io/htnn/api/pkg/plugins" ctrlcfg "mosn.io/htnn/controller/internal/config" "mosn.io/htnn/controller/internal/model" @@ -207,69 +208,123 @@ func GenerateRouteFilter(host *model.VirtualHost, route string, config map[strin func GenerateLDSFilterViaECDS(key string, ldsName string, config map[string]interface{}) *istiov1a3.EnvoyFilter { ef := &istiov1a3.EnvoyFilter{ - Spec: istioapi.EnvoyFilter{ - ConfigPatches: []*istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ - { - ApplyTo: istioapi.EnvoyFilter_HTTP_FILTER, + Spec: istioapi.EnvoyFilter{}, + } + + // Always create a Go filter for ECDS so we don't trigger LDS drain when adding/removing Go Plugins. + // We can't do this for native filters now. + // For TCP proxy use case, the extra golang-filter is harmless, because it only attaches to HCM. + cfg := config[model.ECDSGolangFilter] + if cfg == nil { + cfg = map[string]interface{}{} + } + ecdsName := key + "-" + model.GolangPluginsFilter + ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, + &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istioapi.EnvoyFilter_HTTP_FILTER, + Match: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch{ + ObjectTypes: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istioapi.EnvoyFilter_ListenerMatch{ + Name: ldsName, + FilterChain: &istioapi.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &istioapi.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.filters.network.http_connection_manager", + SubFilter: &istioapi.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "htnn.filters.http.golang", + }, + }, + }, + }, + }, + }, + Patch: &istioapi.EnvoyFilter_Patch{ + Operation: istioapi.EnvoyFilter_Patch_INSERT_BEFORE, + Value: MustNewStruct(map[string]interface{}{ + "name": ecdsName, + "config_discovery": map[string]interface{}{ + "apply_default_config_without_warming": true, + "default_config": map[string]interface{}{ + "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", + "library_id": "fm", + "library_path": ctrlcfg.GoSoPath(), + "plugin_name": "fm", + }, + "config_source": map[string]interface{}{ + "ads": map[string]interface{}{}, + }, + "type_urls": []interface{}{ + "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", + }, + }, + }), + }, + }, + &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istioapi.EnvoyFilter_EXTENSION_CONFIG, + Patch: &istioapi.EnvoyFilter_Patch{ + Operation: istioapi.EnvoyFilter_Patch_ADD, + Value: MustNewStruct(map[string]interface{}{ + "name": ecdsName, + "typed_config": map[string]interface{}{ + "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", + "library_id": "fm", + "library_path": ctrlcfg.GoSoPath(), + "plugin_name": "fm", + "plugin_config": map[string]interface{}{ + "@type": "type.googleapis.com/xds.type.v3.TypedStruct", + "value": cfg, + }, + }, + }), + }, + }, + ) + + if config[model.ECDSListenerFilter] != nil { + cfg, _ := config[model.ECDSListenerFilter].([]*fmModel.FilterConfig) + for _, filter := range cfg { + ecdsName := key + "-" + filter.Name + c, _ := filter.Config.(map[string]interface{}) + typeURL, _ := c["@type"].(string) + ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, + &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istioapi.EnvoyFilter_LISTENER_FILTER, Match: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch{ ObjectTypes: &istioapi.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ Listener: &istioapi.EnvoyFilter_ListenerMatch{ Name: ldsName, - FilterChain: &istioapi.EnvoyFilter_ListenerMatch_FilterChainMatch{ - Filter: &istioapi.EnvoyFilter_ListenerMatch_FilterMatch{ - Name: "envoy.filters.network.http_connection_manager", - SubFilter: &istioapi.EnvoyFilter_ListenerMatch_SubFilterMatch{ - Name: "htnn.filters.http.golang", - }, - }, - }, }, }, }, Patch: &istioapi.EnvoyFilter_Patch{ - Operation: istioapi.EnvoyFilter_Patch_INSERT_BEFORE, + Operation: istioapi.EnvoyFilter_Patch_INSERT_FIRST, Value: MustNewStruct(map[string]interface{}{ - "name": key, + "name": ecdsName, "config_discovery": map[string]interface{}{ - "apply_default_config_without_warming": true, - "default_config": map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", - "library_id": "fm", - "library_path": ctrlcfg.GoSoPath(), - "plugin_name": "fm", - }, "config_source": map[string]interface{}{ "ads": map[string]interface{}{}, }, "type_urls": []interface{}{ - "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", + typeURL, }, }, }), }, }, - }, - }, - } - ef.Spec.ConfigPatches = append(ef.Spec.ConfigPatches, &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ - ApplyTo: istioapi.EnvoyFilter_EXTENSION_CONFIG, - Patch: &istioapi.EnvoyFilter_Patch{ - Operation: istioapi.EnvoyFilter_Patch_ADD, - Value: MustNewStruct(map[string]interface{}{ - "name": key, - "typed_config": map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config", - "library_id": "fm", - "library_path": ctrlcfg.GoSoPath(), - "plugin_name": "fm", - "plugin_config": map[string]interface{}{ - "@type": "type.googleapis.com/xds.type.v3.TypedStruct", - "value": config, + &istioapi.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istioapi.EnvoyFilter_EXTENSION_CONFIG, + Patch: &istioapi.EnvoyFilter_Patch{ + Operation: istioapi.EnvoyFilter_Patch_ADD, + Value: MustNewStruct(map[string]interface{}{ + "name": ecdsName, + "typed_config": filter.Config, + }), }, }, - }), - }, - }) + ) + } + } + return ef } diff --git a/controller/internal/model/model.go b/controller/internal/model/model.go index 09c5b858..1d2374fa 100644 --- a/controller/internal/model/model.go +++ b/controller/internal/model/model.go @@ -36,3 +36,10 @@ type VirtualHost struct { Name string ECDSResourceName string } + +const ( + ECDSGolangFilter = "golang" + ECDSListenerFilter = "listener" + + GolangPluginsFilter = "golang-filter" +) diff --git a/controller/internal/translation/final_state.go b/controller/internal/translation/final_state.go index 495f11c0..dc41f6d2 100644 --- a/controller/internal/translation/final_state.go +++ b/controller/internal/translation/final_state.go @@ -85,10 +85,6 @@ type envoyFilterWrapper struct { info *Info } -func getECDSResourceName(workloadNamespace string, ldsName string) string { - return fmt.Sprintf("htnn-%s-%s-golang-filter", workloadNamespace, ldsName) -} - func toFinalState(_ *Ctx, state *mergedState) (*FinalState, error) { efs := istio.DefaultEnvoyFilters() for _, ef := range efs { diff --git a/controller/internal/translation/merged_state.go b/controller/internal/translation/merged_state.go index 34c14eeb..a5150165 100644 --- a/controller/internal/translation/merged_state.go +++ b/controller/internal/translation/merged_state.go @@ -196,7 +196,7 @@ func translateFilterManagerConfigToPolicyInRDS(fmc *filtermanager.FilterManagerC golangFilterName := "htnn.filters.http.golang" if ctrlcfg.EnableLDSPluginViaECDS() { - golangFilterName = virtualHost.ECDSResourceName + golangFilterName = virtualHost.ECDSResourceName + "-" + model.GolangPluginsFilter } config[golangFilterName] = map[string]interface{}{ "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", @@ -225,18 +225,18 @@ func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManager goFilterManager := &filtermanager.FilterManagerConfig{ Plugins: []*fmModel.FilterConfig{}, } + nativeFilters := map[string][]*fmModel.FilterConfig{ + model.ECDSListenerFilter: {}, + } consumerNeeded := false for _, plugin := range fmc.Plugins { name := plugin.Name p := plugins.LoadPlugin(name) - if p != nil { - // Native plugin is not supported - continue + if p == nil { + // For Go Plugins, only the type is registered + p = plugins.LoadPluginType(name) } - - // For Go Plugins, only the type is registered - p = plugins.LoadPluginType(name) // As we don't reject configuration with unknown plugin to keep compatibility... if p == nil { continue @@ -250,20 +250,45 @@ func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManager } _ = json.Unmarshal(b, &cfg) - plugin.Config = cfg - goFilterManager.Plugins = append(goFilterManager.Plugins, plugin) - _, ok = p.(plugins.ConsumerPlugin) - if ok { - consumerNeeded = true + nativePlugin, ok := p.(plugins.NativePlugin) + if !ok { + plugin.Config = cfg + goFilterManager.Plugins = append(goFilterManager.Plugins, plugin) + _, ok = p.(plugins.ConsumerPlugin) + if ok { + consumerNeeded = true + } + } else { + order := nativePlugin.Order() + if order.Position == plugins.OrderPositionOuter || order.Position == plugins.OrderPositionInner { + // HTTP Native plugin is not supported + continue + } + + url := nativePlugin.ConfigTypeURL() + conf := p.Config() + desc := conf.ProtoReflect().Descriptor() + fieldDescs := desc.Fields() + m, ok := cfg.(map[string]interface{}) + if !ok { + panic(fmt.Sprintf("unexpected type: %s", reflect.TypeOf(cfg))) + } + stripUnknowFields(m, fieldDescs) + + m["@type"] = url + plugin.Config = m + nativeFilters[model.ECDSListenerFilter] = append(nativeFilters[model.ECDSListenerFilter], plugin) } } + if consumerNeeded { goFilterManager.Namespace = nsName.Namespace } if len(goFilterManager.Plugins) > 0 { + cfg := map[string]interface{}{} if goFilterManager.Namespace != "" { - config["namespace"] = goFilterManager.Namespace + cfg["namespace"] = goFilterManager.Namespace } plugins := make([]interface{}, len(goFilterManager.Plugins)) for i, plugin := range goFilterManager.Plugins { @@ -272,9 +297,13 @@ func translateFilterManagerConfigToPolicyInECDS(fmc *filtermanager.FilterManager "config": plugin.Config, } } - config["plugins"] = plugins + cfg["plugins"] = plugins + config[model.ECDSGolangFilter] = cfg } + for category, filters := range nativeFilters { + config[category] = filters + } return config } diff --git a/controller/internal/translation/testdata/translation/l4_native_plugin_only.in.yml b/controller/internal/translation/testdata/translation/l4_native_plugin_only.in.yml new file mode 100644 index 00000000..60e75141 --- /dev/null +++ b/controller/internal/translation/testdata/translation/l4_native_plugin_only.in.yml @@ -0,0 +1,33 @@ +features: + enableLDSPluginViaECDS: true +istioGateway: +- apiVersion: networking.istio.io/v1beta1 + kind: Gateway + metadata: + name: gateway + namespace: default + spec: + selector: + istio: ingressgateway + servers: + - hosts: + - example.com + port: + name: http + number: 80 + protocol: HTTP +filterPolicy: + gateway: + - apiVersion: htnn.mosn.io/v1 + kind: FilterPolicy + metadata: + name: policy + namespace: default + spec: + targetRef: + group: networking.istio.io + kind: Gateway + name: gateway + filters: + tlsInspector: + config: {} diff --git a/controller/internal/translation/testdata/translation/l4_native_plugin_only.out.yml b/controller/internal/translation/testdata/translation/l4_native_plugin_only.out.yml new file mode 100644 index 00000000..2df9289d --- /dev/null +++ b/controller/internal/translation/testdata/translation/l4_native_plugin_only.out.yml @@ -0,0 +1,68 @@ +- metadata: + annotations: + htnn.mosn.io/info: '{"filterpolicies":["default/policy"]}' + creationTimestamp: null + labels: + htnn.mosn.io/created-by: FilterPolicy + name: htnn-h-default + namespace: default + spec: + configPatches: + - applyTo: HTTP_FILTER + match: + listener: + filterChain: + filter: + name: envoy.filters.network.http_connection_manager + subFilter: + name: htnn.filters.http.golang + name: 0.0.0.0_80 + patch: + operation: INSERT_BEFORE + value: + config_discovery: + apply_default_config_without_warming: true + config_source: + ads: {} + default_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config + library_id: fm + library_path: /etc/libgolang.so + plugin_name: fm + type_urls: + - type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config + name: htnn-default-0.0.0.0_80-golang-filter + - applyTo: EXTENSION_CONFIG + patch: + operation: ADD + value: + name: htnn-default-0.0.0.0_80-golang-filter + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config + library_id: fm + library_path: /etc/libgolang.so + plugin_config: + '@type': type.googleapis.com/xds.type.v3.TypedStruct + value: {} + plugin_name: fm + - applyTo: LISTENER_FILTER + match: + listener: + name: 0.0.0.0_80 + patch: + operation: INSERT_FIRST + value: + config_discovery: + config_source: + ads: {} + type_urls: + - type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + name: htnn-default-0.0.0.0_80-tlsInspector + - applyTo: EXTENSION_CONFIG + patch: + operation: ADD + value: + name: htnn-default-0.0.0.0_80-tlsInspector + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + status: {} diff --git a/controller/internal/translation/translation.go b/controller/internal/translation/translation.go index 0803c99f..898b94ca 100644 --- a/controller/internal/translation/translation.go +++ b/controller/internal/translation/translation.go @@ -17,6 +17,7 @@ package translation import ( "context" "encoding/json" + "fmt" "slices" "sort" @@ -67,3 +68,7 @@ type FilterPolicyWrapper struct { type Proxy struct { Namespace string } + +func getECDSResourceName(workloadNamespace string, ldsName string) string { + return fmt.Sprintf("htnn-%s-%s", workloadNamespace, ldsName) +} diff --git a/controller/plugins/plugins.go b/controller/plugins/plugins.go index 9be29d47..85f090f2 100644 --- a/controller/plugins/plugins.go +++ b/controller/plugins/plugins.go @@ -22,5 +22,6 @@ import ( _ "mosn.io/htnn/controller/plugins/fault" _ "mosn.io/htnn/controller/plugins/local_ratelimit" _ "mosn.io/htnn/controller/plugins/lua" + _ "mosn.io/htnn/controller/plugins/tlsinspector" _ "mosn.io/htnn/types/plugins" ) diff --git a/controller/plugins/testdata/network/default.yml b/controller/plugins/testdata/network/default.yml new file mode 100644 index 00000000..1540db70 --- /dev/null +++ b/controller/plugins/testdata/network/default.yml @@ -0,0 +1,14 @@ +# This configuration is shared across different tests and should only contain minimal fields +- apiVersion: networking.istio.io/v1beta1 + kind: Gateway + metadata: + name: default + namespace: default + spec: + servers: + - hosts: + - default.local + port: + name: http + number: 18000 + protocol: HTTP diff --git a/controller/plugins/testdata/network/tls_inspector.in.yml b/controller/plugins/testdata/network/tls_inspector.in.yml new file mode 100644 index 00000000..63a4aedc --- /dev/null +++ b/controller/plugins/testdata/network/tls_inspector.in.yml @@ -0,0 +1,13 @@ +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy + namespace: default +spec: + targetRef: + group: networking.istio.io + kind: Gateway + name: default + filters: + tlsInspector: + config: {} diff --git a/controller/plugins/testdata/network/tls_inspector.out.yml b/controller/plugins/testdata/network/tls_inspector.out.yml new file mode 100644 index 00000000..f6d19776 --- /dev/null +++ b/controller/plugins/testdata/network/tls_inspector.out.yml @@ -0,0 +1,27 @@ +- metadata: + creationTimestamp: null + name: htnn-h-default + namespace: default + spec: + configPatches: + - applyTo: LISTENER_FILTER + match: + listener: + name: 0.0.0.0_18000 + patch: + operation: INSERT_FIRST + value: + config_discovery: + config_source: + ads: {} + type_urls: + - type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + name: htnn-default-0.0.0.0_18000-tlsInspector + - applyTo: EXTENSION_CONFIG + patch: + operation: ADD + value: + name: htnn-default-0.0.0.0_18000-tlsInspector + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + status: {} diff --git a/controller/plugins/tlsinspector/config.go b/controller/plugins/tlsinspector/config.go new file mode 100644 index 00000000..6005135a --- /dev/null +++ b/controller/plugins/tlsinspector/config.go @@ -0,0 +1,32 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlsinspector + +import ( + "mosn.io/htnn/api/pkg/plugins" + "mosn.io/htnn/types/plugins/tlsinspector" +) + +func init() { + plugins.RegisterPlugin(tlsinspector.Name, &plugin{}) +} + +type plugin struct { + tlsinspector.Plugin +} + +func (p *plugin) ConfigTypeURL() string { + return "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector" +} diff --git a/controller/plugins/translation_test.go b/controller/plugins/translation_test.go new file mode 100644 index 00000000..bc798036 --- /dev/null +++ b/controller/plugins/translation_test.go @@ -0,0 +1,118 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugins + +import ( + "context" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/require" + istioapi "istio.io/api/networking/v1alpha3" + istiov1a3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + "sigs.k8s.io/yaml" + + "mosn.io/htnn/controller/internal/istio" + "mosn.io/htnn/controller/internal/translation" + "mosn.io/htnn/controller/tests/pkg" + mosniov1 "mosn.io/htnn/types/apis/v1" +) + +func testName(inputFile string) string { + _, fileName := filepath.Split(inputFile) + return strings.TrimSuffix(fileName, ".in.yml") +} + +func mustUnmarshal(t *testing.T, fn string, out interface{}) { + input, err := os.ReadFile(fn) + require.NoError(t, err) + require.NoError(t, yaml.UnmarshalStrict(input, out, yaml.DisallowUnknownFields)) +} + +func TestNetworkPlugins(t *testing.T) { + inputFiles, err := filepath.Glob(filepath.Join("testdata", "network", "*.in.yml")) + require.NoError(t, err) + + var gw *istiov1a3.Gateway + input := []map[string]interface{}{} + mustUnmarshal(t, filepath.Join("testdata", "network", "default.yml"), &input) + + for _, in := range input { + obj := pkg.MapToObj(in) + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk.Group == "networking.istio.io" && gvk.Kind == "Gateway" { + gw = obj.(*istiov1a3.Gateway) + } + } + + for _, inputFile := range inputFiles { + name := testName(inputFile) + t.Run(name, func(t *testing.T) { + var hfp mosniov1.FilterPolicy + mustUnmarshal(t, inputFile, &hfp) + + s := translation.NewInitState() + s.AddPolicyForIstioGateway(&hfp, gw) + + fs, err := s.Process(context.Background()) + require.NoError(t, err) + + defaultEnvoyFilters := istio.DefaultEnvoyFilters() + for key := range defaultEnvoyFilters { + for _, ef := range fs.EnvoyFilters { + if ef.Name == key.Name { + delete(fs.EnvoyFilters, key) + } + } + } + for _, ef := range fs.EnvoyFilters { + // drop irrelevant fields + ef.Labels = nil + ef.Annotations = nil + kept := []*istioapi.EnvoyFilter_EnvoyConfigObjectPatch{} + for _, cp := range ef.Spec.ConfigPatches { + st := cp.Patch.Value + name := st.AsMap()["name"].(string) + // the golang-filter is harmless if we don't have HCM, because it only attaches to HCM. + if !strings.HasSuffix(name, "golang-filter") { + kept = append(kept, cp) + } + } + ef.Spec.ConfigPatches = kept + } + + var out []*istiov1a3.EnvoyFilter + for _, ef := range fs.EnvoyFilters { + out = append(out, ef) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Namespace != out[j].Namespace { + return out[i].Namespace < out[j].Namespace + } + return out[i].Name < out[j].Name + }) + d, _ := yaml.Marshal(out) + actual := string(d) + + outputFilePath := strings.ReplaceAll(inputFile, ".in.yml", ".out.yml") + d, _ = os.ReadFile(outputFilePath) + want := string(d) + require.Equal(t, want, actual) + }) + } +} diff --git a/e2e/tests/tls_inspector.go b/e2e/tests/tls_inspector.go new file mode 100644 index 00000000..691560e3 --- /dev/null +++ b/e2e/tests/tls_inspector.go @@ -0,0 +1,33 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "mosn.io/htnn/e2e/pkg/suite" +) + +func init() { + suite.Register(suite.Test{ + Run: func(t *testing.T, suite *suite.Suite) { + rsp, err := suite.Get("/echo", nil) + require.NoError(t, err) + require.Equal(t, 200, rsp.StatusCode) + }, + }) +} diff --git a/e2e/tests/tls_inspector.yml b/e2e/tests/tls_inspector.yml new file mode 100644 index 00000000..d57a1942 --- /dev/null +++ b/e2e/tests/tls_inspector.yml @@ -0,0 +1,30 @@ +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + tlsInspector: + config: {} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: test +spec: + parentRefs: + - name: default + namespace: e2e + hostnames: ["localhost"] + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: backend + port: 8080 diff --git a/site/content/en/docs/reference/plugins/tls_inspector.md b/site/content/en/docs/reference/plugins/tls_inspector.md new file mode 100644 index 00000000..4afda359 --- /dev/null +++ b/site/content/en/docs/reference/plugins/tls_inspector.md @@ -0,0 +1,53 @@ +--- +title: TLS Inspector +--- + +## Description + +The `tlsInspector` plugin adds a TLS Inspector listener filter to the targeted Gateway. + +## Attribute + +| | | +|-------|----------| +| Type | General | +| Order | Listener | + +## Configuration + +See the corresponding [Envoy documentation](https://www.envoyproxy.io/docs/envoy/v1.29.5/configuration/listeners/listener_filters/tls_inspector). + +## Usage + +Assumed we have the Gateway below listening to `localhost:10000`: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: default +spec: + gatewayClassName: istio + listeners: + - name: default + hostname: "*" + port: 10000 + protocol: HTTP +``` + +The configuration below adds TLS Inspector listener filter to it: + +```yaml +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + tlsInspector: + config: {} +``` diff --git a/site/content/zh-hans/docs/reference/plugins/tls_inspector.md b/site/content/zh-hans/docs/reference/plugins/tls_inspector.md new file mode 100644 index 00000000..006b0471 --- /dev/null +++ b/site/content/zh-hans/docs/reference/plugins/tls_inspector.md @@ -0,0 +1,53 @@ +--- +title: TLS Inspector +--- + +## 说明 + +`tlsInspector` 插件为目标 Gateway 添加了 TLS 检查器监听过滤器。 + +## 属性 + +| | | +|-------|----------| +| Type | General | +| Order | Listener | + +## 配置 + +请查阅对应的 [Envoy 文档](https://www.envoyproxy.io/docs/envoy/v1.29.5/configuration/listeners/listener_filters/tls_inspector)。 + +## 用法 + +假设我们有以下的 Gateway 在 `localhost:10000` 上监听: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: default +spec: + gatewayClassName: istio + listeners: + - name: default + hostname: "*" + port: 10000 + protocol: HTTP +``` + +下面的配置将 TLS 检查器监听过滤器添加至该网关: + +```yaml +apiVersion: htnn.mosn.io/v1 +kind: FilterPolicy +metadata: + name: policy +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: default + filters: + tlsInspector: + config: {} +``` diff --git a/types/plugins/plugins.go b/types/plugins/plugins.go index 6449e7d1..42fe29b4 100644 --- a/types/plugins/plugins.go +++ b/types/plugins/plugins.go @@ -34,4 +34,5 @@ import ( _ "mosn.io/htnn/types/plugins/lua" _ "mosn.io/htnn/types/plugins/oidc" _ "mosn.io/htnn/types/plugins/opa" + _ "mosn.io/htnn/types/plugins/tlsinspector" ) diff --git a/types/plugins/tlsinspector/config.go b/types/plugins/tlsinspector/config.go new file mode 100644 index 00000000..d9b5c6de --- /dev/null +++ b/types/plugins/tlsinspector/config.go @@ -0,0 +1,44 @@ +// Copyright The HTNN Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlsinspector + +import ( + tls_inspector "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" + + "mosn.io/htnn/api/pkg/filtermanager/api" + "mosn.io/htnn/api/pkg/plugins" +) + +const ( + Name = "tlsInspector" +) + +func init() { + plugins.RegisterPluginType(Name, &Plugin{}) +} + +type Plugin struct { + plugins.PluginMethodDefaultImpl +} + +func (p *Plugin) Order() plugins.PluginOrder { + return plugins.PluginOrder{ + Position: plugins.OrderPositionListener, + } +} + +func (p *Plugin) Config() api.PluginConfig { + return &tls_inspector.TlsInspector{} +}