Repository: envoyproxy/gateway
Pull request #3494 (2024-05-28 14:19:12)

feat: Support connecting to an Envoy Gateway Extension Server over Unix Domain Sockets
Support connecting to an extension server over UDS, allowing for lower latency in the communication between Envoy Gateway and an extension server.

liorokman: /retest

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `5.26316%` with `18 lines` in your changes are missing coverage. Please review.
> Project coverage is 67.31%. Comparing base [(`c2c9b43`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/c2c9b43173895f65583fc9e087b8abac4db1c6be?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`1afeab0`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/1afeab0aaa00c3fc9ccadc4f702a70fdaa1eb263?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 16 commits behind head on main.

| [Files](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/extension/registry/extension\_manager.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?src=pr&el=tree&filepath=internal%2Fextension%2Fregistry%2Fextension_manager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZXh0ZW5zaW9uL3JlZ2lzdHJ5L2V4dGVuc2lvbl9tYW5hZ2VyLmdv) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [api/v1alpha1/validation/envoygateway\_validate.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?src=pr&el=tree&filepath=api%2Fv1alpha1%2Fvalidation%2Fenvoygateway_validate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-YXBpL3YxYWxwaGExL3ZhbGlkYXRpb24vZW52b3lnYXRld2F5X3ZhbGlkYXRlLmdv) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3494      +/-   ##
==========================================
+ Coverage   67.22%   67.31%   +0.08%     
==========================================
  Files         166      167       +1     
  Lines       19720    19942     +222     
==========================================
+ Hits        13257    13423     +166     
- Misses       5507     5554      +47     
- Partials      956      965       +9     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/3494?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

liorokman: /retest

Patch:
diff --git a/api/v1alpha1/envoygateway_types.go b/api/v1alpha1/envoygateway_types.go
index 29b816c5f..882aff68a 100644
--- a/api/v1alpha1/envoygateway_types.go
+++ b/api/v1alpha1/envoygateway_types.go
@@ -482,10 +482,17 @@ type XDSTranslatorHooks struct {
 
 // ExtensionService defines the configuration for connecting to a registered extension service.
 type ExtensionService struct {
+	// BackendEndpoint points to where the extension server can be found.
+	BackendEndpoint `json:",inline"`
+
 	// Host define the extension service hostname.
-	Host string `json:"host"`
+	// Deprecated: use the appropriate transport attribute instead (FQDN,IPv4,Unix)
+	//
+	// +optional
+	Host string `json:"host,omitempty"`
 
 	// Port defines the port the extension service is exposed on.
+	// Deprecated: use the appropriate transport attribute instead (FQDN,IPv4,Unix)
 	//
 	// +optional
 	// +kubebuilder:validation:Minimum=0
diff --git a/api/v1alpha1/validation/envoygateway_validate.go b/api/v1alpha1/validation/envoygateway_validate.go
index 629aae4ae..d4f27633c 100644
--- a/api/v1alpha1/validation/envoygateway_validate.go
+++ b/api/v1alpha1/validation/envoygateway_validate.go
@@ -85,6 +85,18 @@ func ValidateEnvoyGateway(eg *v1alpha1.EnvoyGateway) error {
 			return fmt.Errorf("extension service config is empty")
 		}
 
+		switch {
+		case eg.ExtensionManager.Service.Host == "" && eg.ExtensionManager.Service.FQDN == nil && eg.ExtensionManager.Service.Unix == nil && eg.ExtensionManager.Service.IPv4 == nil:
+			return fmt.Errorf("extension service must contain a configured target")
+
+		case eg.ExtensionManager.Service.FQDN != nil && (eg.ExtensionManager.Service.IPv4 != nil || eg.ExtensionManager.Service.Unix != nil || eg.ExtensionManager.Service.Host != ""),
+			eg.ExtensionManager.Service.IPv4 != nil && (eg.ExtensionManager.Service.FQDN != nil || eg.ExtensionManager.Service.Unix != nil || eg.ExtensionManager.Service.Host != ""),
+			eg.ExtensionManager.Service.Unix != nil && (eg.ExtensionManager.Service.IPv4 != nil || eg.ExtensionManager.Service.FQDN != nil || eg.ExtensionManager.Service.Host != ""):
+
+			return fmt.Errorf("only one backend target can be configured for the extension manager")
+
+		}
+
 		if eg.ExtensionManager.Service.TLS != nil {
 			certificateRefKind := eg.ExtensionManager.Service.TLS.CertificateRef.Kind
 
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 45afe9ffa..8720b70a5 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2014,6 +2014,7 @@ func (in *ExtensionManager) DeepCopy() *ExtensionManager {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExtensionService) DeepCopyInto(out *ExtensionService) {
 	*out = *in
+	in.BackendEndpoint.DeepCopyInto(&out.BackendEndpoint)
 	if in.TLS != nil {
 		in, out := &in.TLS, &out.TLS
 		*out = new(ExtensionTLS)
diff --git a/internal/extension/registry/extension_manager.go b/internal/extension/registry/extension_manager.go
index d00e51ce4..7ef203e5a 100644
--- a/internal/extension/registry/extension_manager.go
+++ b/internal/extension/registry/extension_manager.go
@@ -10,10 +10,12 @@ import (
 	"crypto/x509"
 	"errors"
 	"fmt"
+	"net"
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/test/bufconn"
 	corev1 "k8s.io/api/core/v1"
 	k8scli "sigs.k8s.io/controller-runtime/pkg/client"
 	k8sclicfg "sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -73,6 +75,37 @@ func NewManager(cfg *config.Server) (extTypes.Manager, error) {
 	}, nil
 }
 
+func NewInMemoryManager(cfg v1alpha1.ExtensionManager, server extension.EnvoyGatewayExtensionServer) (extTypes.Manager, func(), error) {
+	if server == nil {
+		return nil, nil, fmt.Errorf("in-memory manager must be passed a server")
+	}
+
+	buffer := 101024 * 1024
+	lis := bufconn.Listen(buffer)
+
+	baseServer := grpc.NewServer()
+	extension.RegisterEnvoyGatewayExtensionServer(baseServer, server)
+	go func() {
+		_ = baseServer.Serve(lis)
+	}()
+	conn, err := grpc.DialContext(context.Background(), "",
+		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
+			return lis.Dial()
+		}), grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		return nil, nil, err
+	}
+	c := func() {
+		lis.Close()
+		baseServer.Stop()
+	}
+
+	return &Manager{
+		extensionConnCache: conn,
+		extension:          cfg,
+	}, c, nil
+}
+
 // HasExtension checks to see whether a given Group and Kind has an
 // associated extension registered for it.
 func (m *Manager) HasExtension(g v1.Group, k v1.Kind) bool {
@@ -86,6 +119,21 @@ func (m *Manager) HasExtension(g v1.Group, k v1.Kind) bool {
 	return false
 }
 
+func getExtensionServerAddress(service *v1alpha1.ExtensionService) string {
+	var serverAddr string
+	switch {
+	case service.FQDN != nil:
+		serverAddr = fmt.Sprintf("%s:%d", service.FQDN.Hostname, service.FQDN.Port)
+	case service.IPv4 != nil:
+		serverAddr = fmt.Sprintf("%s:%d", service.IPv4.Address, service.IPv4.Port)
+	case service.Unix != nil:
+		serverAddr = fmt.Sprintf("unix://%s", service.Unix.Path)
+	case service.Host != "":
+		serverAddr = fmt.Sprintf("%s:%d", service.Host, service.Port)
+	}
+	return serverAddr
+}
+
 // GetPreXDSHookClient checks if the registered extension makes use of a particular hook type that modifies inputs
 // that are used to generate an xDS resource.
 // If the extension makes use of the hook then the XDS Hook Client is returned. If it does not support
@@ -113,7 +161,7 @@ func (m *Manager) GetPreXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) ex
 	}
 
 	if m.extensionConnCache == nil {
-		serverAddr := fmt.Sprintf("%s:%d", ext.Service.Host, ext.Service.Port)
+		serverAddr := getExtensionServerAddress(ext.Service)
 
 		opts, err := setupGRPCOpts(ctx, m.k8sClient, &ext, m.namespace)
 		if err != nil {
@@ -162,7 +210,7 @@ func (m *Manager) GetPostXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) e
 	}
 
 	if m.extensionConnCache == nil {
-		serverAddr := fmt.Sprintf("%s:%d", ext.Service.Host, ext.Service.Port)
+		serverAddr := getExtensionServerAddress(ext.Service)
 
 		opts, err := setupGRPCOpts(ctx, m.k8sClient, &ext, m.namespace)
 		if err != nil {
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 791b7fcbf..58b5e4fca 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -1216,7 +1216,6 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 
 	// Watch Secret CRUDs and process affected EG CRs (Gateway, SecurityPolicy, more in the future).
 	secretPredicates := []predicate.TypedPredicate[*corev1.Secret]{
-		predicate.TypedGenerationChangedPredicate[*corev1.Secret]{},
 		predicate.NewTypedPredicateFuncs(func(s *corev1.Secret) bool {
 			return r.validateSecretForReconcile(s)
 		}),
@@ -1237,7 +1236,6 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 
 	// Watch ConfigMap CRUDs and process affected ClienTraffiPolicies and BackendTLSPolicies.
 	configMapPredicates := []predicate.TypedPredicate[*corev1.ConfigMap]{
-		predicate.TypedGenerationChangedPredicate[*corev1.ConfigMap]{},
 		predicate.NewTypedPredicateFuncs[*corev1.ConfigMap](func(cm *corev1.ConfigMap) bool {
 			return r.validateConfigMapForReconcile(cm)
 		}),


Test patch:
diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go
index aa76a90ec..2823e70a4 100644
--- a/api/v1alpha1/validation/envoygateway_validate_test.go
+++ b/api/v1alpha1/validation/envoygateway_validate_test.go
@@ -547,6 +547,118 @@ func TestValidateEnvoyGateway(t *testing.T) {
 			},
 			expect: false,
 		},
+		{
+			name: "no extension server target set",
+			eg: &v1alpha1.EnvoyGateway{
+				EnvoyGatewaySpec: v1alpha1.EnvoyGatewaySpec{
+					Gateway:  v1alpha1.DefaultGateway(),
+					Provider: v1alpha1.DefaultEnvoyGatewayProvider(),
+					ExtensionManager: &v1alpha1.ExtensionManager{
+						Resources: []v1alpha1.GroupVersionKind{
+							{
+								Group:   "foo.example.io",
+								Version: "v1alpha1",
+								Kind:    "Foo",
+							},
+						},
+						Service: &v1alpha1.ExtensionService{
+							Port: 8080,
+						},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			name: "both host and path targets are set for extension server",
+			eg: &v1alpha1.EnvoyGateway{
+				EnvoyGatewaySpec: v1alpha1.EnvoyGatewaySpec{
+					Gateway:  v1alpha1.DefaultGateway(),
+					Provider: v1alpha1.DefaultEnvoyGatewayProvider(),
+					ExtensionManager: &v1alpha1.ExtensionManager{
+						Resources: []v1alpha1.GroupVersionKind{
+							{
+								Group:   "foo.example.io",
+								Version: "v1alpha1",
+								Kind:    "Foo",
+							},
+						},
+						Service: &v1alpha1.ExtensionService{
+							BackendEndpoint: v1alpha1.BackendEndpoint{
+								FQDN: &v1alpha1.FQDNEndpoint{
+									Hostname: "foo.example.com",
+									Port:     8080,
+								},
+								Unix: &v1alpha1.UnixSocket{
+									Path: "/some/path",
+								},
+							},
+						},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			name: "multiple backend targets are set for extension server",
+			eg: &v1alpha1.EnvoyGateway{
+				EnvoyGatewaySpec: v1alpha1.EnvoyGatewaySpec{
+					Gateway:  v1alpha1.DefaultGateway(),
+					Provider: v1alpha1.DefaultEnvoyGatewayProvider(),
+					ExtensionManager: &v1alpha1.ExtensionManager{
+						Resources: []v1alpha1.GroupVersionKind{
+							{
+								Group:   "foo.example.io",
+								Version: "v1alpha1",
+								Kind:    "Foo",
+							},
+						},
+						Service: &v1alpha1.ExtensionService{
+							BackendEndpoint: v1alpha1.BackendEndpoint{
+								FQDN: &v1alpha1.FQDNEndpoint{
+									Hostname: "foo.example.com",
+									Port:     8080,
+								},
+								IPv4: &v1alpha1.IPv4Endpoint{
+									Address: "10.9.8.7",
+									Port:    8080,
+								},
+							},
+						},
+					},
+				},
+			},
+			expect: false,
+		},
+		{
+			name: "both host and path targets are set for extension server",
+			eg: &v1alpha1.EnvoyGateway{
+				EnvoyGatewaySpec: v1alpha1.EnvoyGatewaySpec{
+					Gateway:  v1alpha1.DefaultGateway(),
+					Provider: v1alpha1.DefaultEnvoyGatewayProvider(),
+					ExtensionManager: &v1alpha1.ExtensionManager{
+						Resources: []v1alpha1.GroupVersionKind{
+							{
+								Group:   "foo.example.io",
+								Version: "v1alpha1",
+								Kind:    "Foo",
+							},
+						},
+						Service: &v1alpha1.ExtensionService{
+							Host: "foo.example.com",
+							Port: 8080,
+							BackendEndpoint: v1alpha1.BackendEndpoint{
+								FQDN: &v1alpha1.FQDNEndpoint{
+									Hostname: "foo.example.com",
+									Port:     8080,
+								},
+							},
+						},
+					},
+				},
+			},
+			expect: false,
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/internal/extension/registry/extension_manager_test.go b/internal/extension/registry/extension_manager_test.go
new file mode 100644
index 000000000..28e7d84ff
--- /dev/null
+++ b/internal/extension/registry/extension_manager_test.go
@@ -0,0 +1,74 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package registry
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/envoyproxy/gateway/api/v1alpha1"
+)
+
+func TestGetExtensionServerAddress(t *testing.T) {
+	tests := []struct {
+		Name     string
+		Service  *v1alpha1.ExtensionService
+		Expected string
+	}{
+		{
+			Name: "has an FQDN",
+			Service: &v1alpha1.ExtensionService{
+				BackendEndpoint: v1alpha1.BackendEndpoint{
+					FQDN: &v1alpha1.FQDNEndpoint{
+						Hostname: "extserver.svc.cluster.local",
+						Port:     5050,
+					},
+				},
+			},
+			Expected: "extserver.svc.cluster.local:5050",
+		},
+		{
+			Name: "has an IPv4",
+			Service: &v1alpha1.ExtensionService{
+				BackendEndpoint: v1alpha1.BackendEndpoint{
+					IPv4: &v1alpha1.IPv4Endpoint{
+						Address: "10.10.10.10",
+						Port:    5050,
+					},
+				},
+			},
+			Expected: "10.10.10.10:5050",
+		},
+		{
+			Name: "has a Unix path",
+			Service: &v1alpha1.ExtensionService{
+				BackendEndpoint: v1alpha1.BackendEndpoint{
+					Unix: &v1alpha1.UnixSocket{
+						Path: "/some/path",
+					},
+				},
+			},
+			Expected: "unix:///some/path",
+		},
+		{
+			Name: "has a Unix path",
+			Service: &v1alpha1.ExtensionService{
+				Host: "foo.bar",
+				Port: 5050,
+			},
+			Expected: "foo.bar:5050",
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.Name, func(t *testing.T) {
+			out := getExtensionServerAddress(tc.Service)
+			require.Equal(t, tc.Expected, out)
+		})
+	}
+}
diff --git a/internal/extension/testutils/manager.go b/internal/extension/testutils/manager.go
deleted file mode 100644
index 7185d5873..000000000
--- a/internal/extension/testutils/manager.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Envoy Gateway Authors
-// SPDX-License-Identifier: Apache-2.0
-// The full text of the Apache license is available in the LICENSE file at
-// the root of the repo.
-
-package testutils
-
-import (
-	v1 "sigs.k8s.io/gateway-api/apis/v1"
-
-	"github.com/envoyproxy/gateway/api/v1alpha1"
-	extType "github.com/envoyproxy/gateway/internal/extension/types"
-)
-
-var _ extType.Manager = (*Manager)(nil)
-
-type Manager struct {
-	extension v1alpha1.ExtensionManager
-}
-
-func NewManager(ext v1alpha1.ExtensionManager) extType.Manager {
-	return &Manager{
-		extension: ext,
-	}
-}
-
-func (m *Manager) HasExtension(g v1.Group, k v1.Kind) bool {
-	extension := m.extension
-	// TODO: not currently checking the version since extensionRef only supports group and kind.
-	for _, gvk := range extension.Resources {
-		if g == v1.Group(gvk.Group) && k == v1.Kind(gvk.Kind) {
-			return true
-		}
-	}
-	return false
-}
-
-func (m *Manager) GetPreXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) extType.XDSHookClient {
-	if m.extension.Hooks == nil {
-		return nil
-	}
-
-	if m.extension.Hooks.XDSTranslator == nil {
-		return nil
-	}
-
-	for _, hook := range m.extension.Hooks.XDSTranslator.Pre {
-		if xdsHookType == hook {
-			return &XDSHookClient{}
-		}
-	}
-	return nil
-}
-
-func (m *Manager) GetPostXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) extType.XDSHookClient {
-	if m.extension.Hooks == nil {
-		return nil
-	}
-
-	if m.extension.Hooks.XDSTranslator == nil {
-		return nil
-	}
-
-	for _, hook := range m.extension.Hooks.XDSTranslator.Post {
-		if xdsHookType == hook {
-			return &XDSHookClient{}
-		}
-	}
-	return nil
-}
diff --git a/internal/gatewayapi/runner/runner_test.go b/internal/gatewayapi/runner/runner_test.go
index 204d4e843..5f3bc2a65 100644
--- a/internal/gatewayapi/runner/runner_test.go
+++ b/internal/gatewayapi/runner/runner_test.go
@@ -19,9 +19,10 @@ import (
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
-	"github.com/envoyproxy/gateway/internal/extension/testutils"
+	"github.com/envoyproxy/gateway/internal/extension/registry"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/message"
+	pb "github.com/envoyproxy/gateway/proto/extension"
 )
 
 func TestRunner(t *testing.T) {
@@ -31,12 +32,15 @@ func TestRunner(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 	// Start
@@ -115,12 +119,15 @@ func TestDeleteStatusKeys(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 
@@ -205,12 +212,15 @@ func TestDeleteAllStatusKeys(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 
diff --git a/internal/extension/testutils/hooks.go b/internal/xds/translator/extensionserver_test.go
similarity index 60%
rename from internal/extension/testutils/hooks.go
rename to internal/xds/translator/extensionserver_test.go
index b7b38c166..9a65c9954 100644
--- a/internal/extension/testutils/hooks.go
+++ b/internal/xds/translator/extensionserver_test.go
@@ -3,9 +3,10 @@
 // The full text of the Apache license is available in the LICENSE file at
 // the root of the repo.
 
-package testutils
+package translator
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
@@ -16,24 +17,36 @@ import (
 	listenerV3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	routeV3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	tlsV3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
-	"github.com/golang/protobuf/proto"
+	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/types/known/durationpb"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	pb "github.com/envoyproxy/gateway/proto/extension"
 )
 
-type XDSHookClient struct{}
+type testingExtensionServer struct {
+	pb.UnimplementedEnvoyGatewayExtensionServer
+}
 
 // PostRouteModifyHook returns a modified version of the route using context info and the passed in extensionResources
-func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames []string, extensionResources []*unstructured.Unstructured) (*routeV3.Route, error) {
+func (t *testingExtensionServer) PostRouteModify(_ context.Context, req *pb.PostRouteModifyRequest) (*pb.PostRouteModifyResponse, error) {
 	// Simulate an error an extension may return
-	if route.Name == "extension-post-xdsroute-hook-error" {
-		return nil, errors.New("route hook resource error")
+	if req.Route.Name == "extension-post-xdsroute-hook-error" {
+		return &pb.PostRouteModifyResponse{
+			Route: req.Route,
+		}, errors.New("route hook resource error")
 	}
 
 	// Setup a new route to avoid operating directly on the passed in pointer for better test coverage that the
 	// route we are returning gets used properly
-	modifiedRoute := proto.Clone(route).(*routeV3.Route)
-	for _, extensionResource := range extensionResources {
+	modifiedRoute := proto.Clone(req.Route).(*routeV3.Route)
+	for _, extensionResourceBytes := range req.PostRouteContext.ExtensionResources {
+		extensionResource := unstructured.Unstructured{}
+		if err := extensionResource.UnmarshalJSON(extensionResourceBytes.UnstructuredBytes); err != nil {
+			return &pb.PostRouteModifyResponse{
+				Route: req.Route,
+			}, err
+		}
 		modifiedRoute.ResponseHeadersToAdd = append(modifiedRoute.ResponseHeadersToAdd,
 			&coreV3.HeaderValueOption{
 				Header: &coreV3.HeaderValue{
@@ -44,7 +57,7 @@ func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames
 			&coreV3.HeaderValueOption{
 				Header: &coreV3.HeaderValue{
 					Key:   "mock-extension-was-here-route-hostnames",
-					Value: strings.Join(routeHostnames, ", "),
+					Value: strings.Join(req.PostRouteContext.Hostnames, ", "),
 				},
 			},
 			&coreV3.HeaderValueOption{
@@ -73,19 +86,23 @@ func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames
 			},
 		)
 	}
-	return modifiedRoute, nil
+	return &pb.PostRouteModifyResponse{
+		Route: modifiedRoute,
+	}, nil
 }
 
 // PostVirtualHostModifyHook returns a modified version of the virtualhost with a new route injected
-func (c *XDSHookClient) PostVirtualHostModifyHook(vh *routeV3.VirtualHost) (*routeV3.VirtualHost, error) {
+func (t *testingExtensionServer) PostVirtualHostModify(_ context.Context, req *pb.PostVirtualHostModifyRequest) (*pb.PostVirtualHostModifyResponse, error) {
 	// Only make the change when the VirtualHost's name matches the expected testdata
 	// This prevents us from having to update every single testfile.out
-	if vh.Name == "extension-post-xdsvirtualhost-hook-error/*" {
-		return nil, fmt.Errorf("extension post xds virtual host hook error")
-	} else if vh.Name == "extension-listener" {
+	if req.VirtualHost.Name == "extension-post-xdsvirtualhost-hook-error/*" {
+		return &pb.PostVirtualHostModifyResponse{
+			VirtualHost: req.VirtualHost,
+		}, fmt.Errorf("extension post xds virtual host hook error")
+	} else if req.VirtualHost.Name == "extension-listener" {
 		// Setup a new VirtualHost to avoid operating directly on the passed in pointer for better test coverage that the
 		// VirtualHost we are returning gets used properly
-		modifiedVH := proto.Clone(vh).(*routeV3.VirtualHost)
+		modifiedVH := proto.Clone(req.VirtualHost).(*routeV3.VirtualHost)
 		modifiedVH.Routes = append(modifiedVH.Routes, &routeV3.Route{
 			Name: "mock-extension-inserted-route",
 			Action: &routeV3.Route_DirectResponse{
@@ -94,31 +111,41 @@ func (c *XDSHookClient) PostVirtualHostModifyHook(vh *routeV3.VirtualHost) (*rou
 				},
 			},
 		})
-		return modifiedVH, nil
+		return &pb.PostVirtualHostModifyResponse{
+			VirtualHost: modifiedVH,
+		}, nil
 	}
-	return vh, nil
+	return &pb.PostVirtualHostModifyResponse{
+		VirtualHost: req.VirtualHost,
+	}, nil
 }
 
 // PostHTTPListenerModifyHook returns a modified version of the listener with a changed statprefix of the listener
 // A more useful use-case for an extension would be looping through the FilterChains to find the
 // HTTPConnectionManager(s) and inject a custom HTTPFilter, but that for testing purposes we don't need to make a complex change
-func (c *XDSHookClient) PostHTTPListenerModifyHook(l *listenerV3.Listener) (*listenerV3.Listener, error) {
+func (t *testingExtensionServer) PostHTTPListenerModify(_ context.Context, req *pb.PostHTTPListenerModifyRequest) (*pb.PostHTTPListenerModifyResponse, error) {
 	// Only make the change when the listener's name matches the expected testdata
 	// This prevents us from having to update every single testfile.out
-	if l.Name == "extension-post-xdslistener-hook-error" {
-		return nil, fmt.Errorf("extension post xds listener hook error")
-	} else if l.Name == "extension-listener" {
+	if req.Listener.Name == "extension-post-xdslistener-hook-error" {
+		return &pb.PostHTTPListenerModifyResponse{
+			Listener: req.Listener,
+		}, fmt.Errorf("extension post xds listener hook error")
+	} else if req.Listener.Name == "extension-listener" {
 		// Setup a new Listener to avoid operating directly on the passed in pointer for better test coverage that the
 		// Listener we are returning gets used properly
-		modifiedListener := proto.Clone(l).(*listenerV3.Listener)
+		modifiedListener := proto.Clone(req.Listener).(*listenerV3.Listener)
 		modifiedListener.StatPrefix = "mock-extension-inserted-prefix"
-		return modifiedListener, nil
+		return &pb.PostHTTPListenerModifyResponse{
+			Listener: modifiedListener,
+		}, nil
 	}
-	return l, nil
+	return &pb.PostHTTPListenerModifyResponse{
+		Listener: req.Listener,
+	}, nil
 }
 
 // PostTranslateModifyHook inserts and overrides some clusters/secrets
-func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, secrets []*tlsV3.Secret) ([]*clusterV3.Cluster, []*tlsV3.Secret, error) {
+func (t *testingExtensionServer) PostTranslateModify(_ context.Context, req *pb.PostTranslateModifyRequest) (*pb.PostTranslateModifyResponse, error) {
 	extensionSvcEndpoint := &endpointV3.LbEndpoint_Endpoint{
 		Endpoint: &endpointV3.Endpoint{
 			Address: &coreV3.Address{
@@ -135,13 +162,18 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	}
 
-	for idx, cluster := range clusters {
+	response := &pb.PostTranslateModifyResponse{
+		Clusters: make([]*clusterV3.Cluster, len(req.Clusters)),
+		Secrets:  make([]*tlsV3.Secret, len(req.Secrets)),
+	}
+	for idx, cluster := range req.Clusters {
+		response.Clusters[idx] = proto.Clone(cluster).(*clusterV3.Cluster)
 		if cluster.Name == "first-route" {
-			clusters[idx].ConnectTimeout = &durationpb.Duration{Seconds: 30}
+			response.Clusters[idx].ConnectTimeout = &durationpb.Duration{Seconds: 30}
 		}
 	}
 
-	clusters = append(clusters, &clusterV3.Cluster{
+	response.Clusters = append(response.Clusters, &clusterV3.Cluster{
 		Name: "mock-extension-injected-cluster",
 		LoadAssignment: &endpointV3.ClusterLoadAssignment{
 			ClusterName: "mock-extension-injected-cluster",
@@ -157,7 +189,10 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	})
 
-	secrets = append(secrets, &tlsV3.Secret{
+	for idx, secret := range req.Secrets {
+		response.Secrets[idx] = proto.Clone(secret).(*tlsV3.Secret)
+	}
+	response.Secrets = append(response.Secrets, &tlsV3.Secret{
 		Name: "mock-extension-injected-secret",
 		Type: &tlsV3.Secret_GenericSecret{
 			GenericSecret: &tlsV3.GenericSecret{
@@ -170,5 +205,5 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	})
 
-	return clusters, secrets, nil
+	return response, nil
 }
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index 60dcf8636..c77145d5b 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -23,7 +23,7 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/api/v1alpha1"
-	"github.com/envoyproxy/gateway/internal/extension/testutils"
+	"github.com/envoyproxy/gateway/internal/extension/registry"
 	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils/field"
@@ -199,13 +199,13 @@ func TestTranslateRateLimitConfig(t *testing.T) {
 func TestTranslateXdsWithExtension(t *testing.T) {
 	testConfigs := map[string]testFileConfig{
 		"http-route-extension-route-error": {
-			errMsg: "route hook resource error",
+			errMsg: "rpc error: code = Unknown desc = route hook resource error",
 		},
 		"http-route-extension-virtualhost-error": {
-			errMsg: "extension post xds virtual host hook error",
+			errMsg: "rpc error: code = Unknown desc = extension post xds virtual host hook error",
 		},
 		"http-route-extension-listener-error": {
-			errMsg: "extension post xds listener hook error",
+			errMsg: "rpc error: code = Unknown desc = extension post xds listener hook error",
 		},
 	}
 
@@ -248,7 +248,10 @@ func TestTranslateXdsWithExtension(t *testing.T) {
 					},
 				},
 			}
-			extMgr := testutils.NewManager(ext)
+
+			extMgr, closeFunc, err := registry.NewInMemoryManager(ext, &testingExtensionServer{})
+			require.NoError(t, err)
+			defer closeFunc()
 			tr.ExtensionManager = &extMgr
 
 			tCtx, err := tr.Translate(x)
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 2652b19b7..0ba1f5677 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -286,6 +286,7 @@ corresponding to Envoy's Address: https://www.envoyproxy.io/docs/envoy/latest/ap
 
 _Appears in:_
 - [BackendSpec](#backendspec)
+- [ExtensionService](#extensionservice)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
@@ -1514,8 +1515,11 @@ _Appears in:_
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
-| `host` | _string_ |  true  | Host define the extension service hostname. |
-| `port` | _integer_ |  false  | Port defines the port the extension service is exposed on. |
+| `fqdn` | _[FQDNEndpoint](#fqdnendpoint)_ |  false  | FQDN defines a FQDN endpoint |
+| `ipv4` | _[IPv4Endpoint](#ipv4endpoint)_ |  false  | IPv4 defines an IPv4 endpoint |
+| `unix` | _[UnixSocket](#unixsocket)_ |  false  | Unix defines the unix domain socket endpoint |
+| `host` | _string_ |  false  | Host define the extension service hostname.<br />Deprecated: use the appropriate transport attribute instead (FQDN,IPv4,Unix) |
+| `port` | _integer_ |  false  | Port defines the port the extension service is exposed on.<br />Deprecated: use the appropriate transport attribute instead (FQDN,IPv4,Unix) |
 | `tls` | _[ExtensionTLS](#extensiontls)_ |  false  | TLS defines TLS configuration for communication between Envoy Gateway and<br />the extension service. |
 
 
@@ -1542,6 +1546,7 @@ https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#
 
 _Appears in:_
 - [BackendEndpoint](#backendendpoint)
+- [ExtensionService](#extensionservice)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
@@ -1925,6 +1930,7 @@ https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#
 
 _Appears in:_
 - [BackendEndpoint](#backendendpoint)
+- [ExtensionService](#extensionservice)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |
@@ -3520,6 +3526,7 @@ https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#
 
 _Appears in:_
 - [BackendEndpoint](#backendendpoint)
+- [ExtensionService](#extensionservice)
 
 | Field | Type | Required | Description |
 | ---   | ---  | ---      | ---         |