Skip to content

Commit e36675b

Browse files
authored
feat(envoyproxy/serviceextensions): add Unix domain socket support (#4463)
### What does this PR do? Adds support for Unix domain socket (UDS) as an alternative transport for the gRPC callout server in the ASM Service Extension. When `DD_SERVICE_EXTENSION_UDS_PATH` is set, the server binds to the specified socket path instead of listening on a TCP `host:port`. ### Motivation When running the Service Extension alongside a self-managed Envoy on the same host, a Unix domain socket avoids the need for TLS and reduces network overhead compared to a loopback TCP connection. This is useful for local Envoy deployments that are not operated through GCP Service Extensions. ### Changes - Add `extensionSocketPath string` field to `serviceExtensionConfig` - Read `DD_SERVICE_EXTENSION_UDS_PATH` env var in `loadConfig` (empty = TCP mode, no change to existing behaviour) - In `startGPRCSsl`: use `net.Listen("unix", path)` when socket path is set; warn and skip TLS in UDS mode; defer socket file cleanup to cover all exit paths (clean shutdown and `Serve` errors); log a warning for unexpected errors when removing stale socket files before bind - Register `DD_SERVICE_EXTENSION_UDS_PATH` in `internal/env/supported_configurations.json` and regenerate `supported_configurations.gen.go` - Fix pre-existing test isolation bug: `allKeys` had wrong TLS env var names (`_CERT`/`_KEY` instead of `_CERT_FILE`/`_KEY_FILE`) - Document the new env var in `README.md` with an Envoy YAML config example ### Reviewer's Checklist - [x] Changed code has unit tests for its functionality at or near 100% coverage. - [ ] [System-Tests](https://github.com/DataDog/system-tests/) covering this feature have been added and enabled with the va.b.c-dev version tag. - [ ] There is a benchmark for any new code, or changes to existing code. - [ ] If this interacts with the agent in a new way, a system test has been added. - [x] New code is free of linting errors. You can check this by running `make lint` locally. - [x] New code doesn't break existing tests. You can check this by running `make test` locally. - [ ] Add an appropriate team label so this PR gets put in the right place for the release notes. - [x] All generated files are up to date. You can check this by running `make generate` locally. - [ ] Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running `make fix-modules` locally.
2 parents 887cc43 + 0cad2cc commit e36675b

5 files changed

Lines changed: 75 additions & 12 deletions

File tree

contrib/envoyproxy/go-control-plane/cmd/serviceextensions/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The ASM Service Extension expose some configuration. The configuration can be tw
3232
| `DD_SERVICE_EXTENSION_TLS` | `true` | Enable the gRPC TLS layer. Do not modify if you are using GCP. |
3333
| `DD_SERVICE_EXTENSION_TLS_KEY_FILE` | `localhost.key` | Change the default gRPC TLS layer key. Do not modify if you are using GCP. |
3434
| `DD_SERVICE_EXTENSION_TLS_CERT_FILE` | `localhost.crt` | Change the default gRPC TLS layer cert. Do not modify if you are using GCP. |
35+
| `DD_SERVICE_EXTENSION_UDS_PATH` | _(unset)_ | Path to a Unix domain socket for the gRPC server. When set, overrides `DD_SERVICE_EXTENSION_HOST`/`PORT` and TLS is disabled. Only for self-managed Envoy deployments. |
3536

3637
> The Service Extension need to be connected to a deployed [Datadog agent](https://docs.datadoghq.com/agent).
3738
@@ -45,3 +46,23 @@ The ASM Service Extension expose some configuration. The configuration can be tw
4546
The Envoy of GCP is configured to communicate to the Service Extension with TLS.
4647

4748
`localhost` self signed certificates are generated and bundled into the App & API Protection Service Extension docker image and loaded at the start of the gRPC server.
49+
50+
### Unix Domain Socket
51+
52+
When running alongside a self-managed Envoy on the same host, the gRPC server can listen on a Unix domain socket instead of a TCP port. This avoids the need for TLS and reduces network overhead.
53+
54+
Set `DD_SERVICE_EXTENSION_UDS_PATH` to the desired socket path:
55+
56+
```sh
57+
DD_SERVICE_EXTENSION_UDS_PATH=/var/run/dd-service-extension.sock ./service-extensions-callout
58+
```
59+
60+
Configure Envoy to connect to the same socket:
61+
62+
```yaml
63+
grpc_service:
64+
google_grpc:
65+
target_uri: "unix:///var/run/dd-service-extension.sock"
66+
```
67+
68+
> **Note:** TLS (`DD_SERVICE_EXTENSION_TLS`) is ignored when a Unix domain socket is configured. Unix domain sockets are local to the host and do not require transport-layer encryption. This option is **not** supported with GCP Service Extensions.

contrib/envoyproxy/go-control-plane/cmd/serviceextensions/main.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type tlsConfig struct {
4444
type serviceExtensionConfig struct {
4545
extensionPort string
4646
extensionHost string
47+
extensionSocketPath string
4748
healthcheckPort string
4849
observabilityMode bool
4950
bodyParsingSizeLimit *int
@@ -102,6 +103,7 @@ func loadConfig() serviceExtensionConfig {
102103
enableTLS := boolEnv("DD_SERVICE_EXTENSION_TLS", true)
103104
keyFile := stringEnv("DD_SERVICE_EXTENSION_TLS_KEY_FILE", "localhost.key")
104105
certFile := stringEnv("DD_SERVICE_EXTENSION_TLS_CERT_FILE", "localhost.crt")
106+
socketPath := stringEnv("DD_SERVICE_EXTENSION_UDS_PATH", "")
105107

106108
extensionPortStr := strconv.FormatInt(int64(extensionPortInt), 10)
107109
healthcheckPortStr := strconv.FormatInt(int64(healthcheckPortInt), 10)
@@ -117,6 +119,7 @@ func loadConfig() serviceExtensionConfig {
117119
return serviceExtensionConfig{
118120
extensionPort: extensionPortStr,
119121
extensionHost: extensionHostStr,
122+
extensionSocketPath: socketPath,
120123
healthcheckPort: healthcheckPortStr,
121124
observabilityMode: observabilityMode,
122125
bodyParsingSizeLimit: bodyParsingSizeLimit,
@@ -193,14 +196,32 @@ func startHealthCheck(ctx context.Context, config serviceExtensionConfig) error
193196
}
194197

195198
func startGPRCSsl(ctx context.Context, service extproc.ExternalProcessorServer, config serviceExtensionConfig) error {
196-
lis, err := net.Listen("tcp", config.extensionHost+":"+config.extensionPort)
197-
if err != nil {
198-
return fmt.Errorf("gRPC server: %s", err)
199+
var (
200+
lis net.Listener
201+
err error
202+
)
203+
204+
if config.extensionSocketPath != "" {
205+
// Unix domain socket mode: remove any stale socket file before binding.
206+
if removeErr := os.Remove(config.extensionSocketPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
207+
log.Warn("service_extension: could not remove stale socket file %s: %s\n", config.extensionSocketPath, removeErr)
208+
}
209+
lis, err = net.Listen("unix", config.extensionSocketPath)
210+
if err != nil {
211+
return fmt.Errorf("gRPC server: %s", err)
212+
}
213+
log.Info("service_extension: callout gRPC server started on unix://%s\n", config.extensionSocketPath)
214+
} else {
215+
lis, err = net.Listen("tcp", config.extensionHost+":"+config.extensionPort)
216+
if err != nil {
217+
return fmt.Errorf("gRPC server: %s", err)
218+
}
219+
log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort)
199220
}
200221

201222
var serverOptions []grpc.ServerOption
202223

203-
if config.tls != nil {
224+
if config.tls != nil && config.extensionSocketPath == "" {
204225
cert, err := tls.LoadX509KeyPair(config.tls.certFile, config.tls.keyFile)
205226
if err != nil {
206227
return fmt.Errorf("failed to load key pair: %s", err)
@@ -221,13 +242,16 @@ func startGPRCSsl(ctx context.Context, service extproc.ExternalProcessorServer,
221242
BodyParsingSizeLimit: config.bodyParsingSizeLimit,
222243
})
223244

245+
if config.extensionSocketPath != "" {
246+
defer os.Remove(config.extensionSocketPath)
247+
}
248+
224249
go func() {
225250
<-ctx.Done()
226251
grpcServer.GracefulStop()
227252
}()
228253

229254
extproc.RegisterExternalProcessorServer(grpcServer, appsecEnvoyExternalProcessorServer)
230-
log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort)
231255
if err := grpcServer.Serve(lis); err != nil {
232256
return fmt.Errorf("error starting gRPC server: %s", err)
233257
}

contrib/envoyproxy/go-control-plane/cmd/serviceextensions/main_test.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func TestLoadConfig_VariousCases(t *testing.T) {
7777
extensionPort string
7878
healthcheckPort string
7979
extensionHost string
80+
extensionSocketPath string
8081
observabilityMode bool
8182
bodyParsingSizeLimit *int
8283
tlsEnabled bool
@@ -92,7 +93,7 @@ func TestLoadConfig_VariousCases(t *testing.T) {
9293
{
9394
name: "defaults",
9495
env: nil,
95-
want: want{"443", "80", "0.0.0.0", false, nil, true, "localhost.crt", "localhost.key"},
96+
want: want{extensionPort: "443", healthcheckPort: "80", extensionHost: "0.0.0.0", tlsEnabled: true, tlsCertFile: "localhost.crt", tlsKeyFile: "localhost.key"},
9697
},
9798
{
9899
name: "valid overrides",
@@ -103,7 +104,7 @@ func TestLoadConfig_VariousCases(t *testing.T) {
103104
"DD_SERVICE_EXTENSION_OBSERVABILITY_MODE": "true",
104105
"DD_APPSEC_BODY_PARSING_SIZE_LIMIT": "100000000",
105106
},
106-
want: want{"1234", "4321", "127.0.0.1", true, intPtr(100000000), true, "localhost.crt", "localhost.key"},
107+
want: want{extensionPort: "1234", healthcheckPort: "4321", extensionHost: "127.0.0.1", observabilityMode: true, bodyParsingSizeLimit: intPtr(100000000), tlsEnabled: true, tlsCertFile: "localhost.crt", tlsKeyFile: "localhost.key"},
107108
},
108109
{
109110
name: "bad values fall back",
@@ -114,22 +115,29 @@ func TestLoadConfig_VariousCases(t *testing.T) {
114115
"DD_APPSEC_BODY_PARSING_SIZE_LIMIT": "notanint",
115116
"DD_SERVICE_EXTENSION_HOST": "notanip",
116117
},
117-
want: want{"443", "80", "0.0.0.0", false, nil, true, "localhost.crt", "localhost.key"},
118+
want: want{extensionPort: "443", healthcheckPort: "80", extensionHost: "0.0.0.0", tlsEnabled: true, tlsCertFile: "localhost.crt", tlsKeyFile: "localhost.key"},
118119
},
119120
{
120121
name: "no-tls",
121122
env: map[string]string{
122123
"DD_SERVICE_EXTENSION_TLS": "false",
123124
},
124-
want: want{"443", "80", "0.0.0.0", false, nil, false, "localhost.key", "localhost.crt"},
125+
want: want{extensionPort: "443", healthcheckPort: "80", extensionHost: "0.0.0.0", tlsEnabled: false},
125126
},
126127
{
127128
name: "custom-tls",
128129
env: map[string]string{
129130
"DD_SERVICE_EXTENSION_TLS_KEY_FILE": "/tls/tls.key",
130131
"DD_SERVICE_EXTENSION_TLS_CERT_FILE": "/tls/tls.crt",
131132
},
132-
want: want{"443", "80", "0.0.0.0", false, nil, true, "/tls/tls.crt", "/tls/tls.key"},
133+
want: want{extensionPort: "443", healthcheckPort: "80", extensionHost: "0.0.0.0", tlsEnabled: true, tlsCertFile: "/tls/tls.crt", tlsKeyFile: "/tls/tls.key"},
134+
},
135+
{
136+
name: "unix domain socket",
137+
env: map[string]string{
138+
"DD_SERVICE_EXTENSION_UDS_PATH": "/var/run/dd-se.sock",
139+
},
140+
want: want{extensionPort: "443", healthcheckPort: "80", extensionHost: "0.0.0.0", extensionSocketPath: "/var/run/dd-se.sock", tlsEnabled: true, tlsCertFile: "localhost.crt", tlsKeyFile: "localhost.key"},
133141
},
134142
}
135143

@@ -139,9 +147,10 @@ func TestLoadConfig_VariousCases(t *testing.T) {
139147
"DD_SERVICE_EXTENSION_HOST",
140148
"DD_SERVICE_EXTENSION_OBSERVABILITY_MODE",
141149
"DD_APPSEC_BODY_PARSING_SIZE_LIMIT",
142-
"DD_SERVICE_EXTENSION_TLS_CERT",
143-
"DD_SERVICE_EXTENSION_TLS_KEY",
150+
"DD_SERVICE_EXTENSION_TLS_CERT_FILE",
151+
"DD_SERVICE_EXTENSION_TLS_KEY_FILE",
144152
"DD_SERVICE_EXTENSION_TLS",
153+
"DD_SERVICE_EXTENSION_UDS_PATH",
145154
}
146155

147156
for _, tc := range cases {
@@ -153,6 +162,7 @@ func TestLoadConfig_VariousCases(t *testing.T) {
153162
assert.Equal(t, tc.want.extensionPort, cfg.extensionPort, "extensionPort")
154163
assert.Equal(t, tc.want.healthcheckPort, cfg.healthcheckPort, "healthcheckPort")
155164
assert.Equal(t, tc.want.extensionHost, cfg.extensionHost, "extensionHost")
165+
assert.Equal(t, tc.want.extensionSocketPath, cfg.extensionSocketPath, "extensionSocketPath")
156166
assert.Equal(t, tc.want.observabilityMode, cfg.observabilityMode, "observabilityMode")
157167
assert.Equal(t, tc.want.bodyParsingSizeLimit, cfg.bodyParsingSizeLimit, "bodyParsingSizeLimit")
158168

internal/env/supported_configurations.gen.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/env/supported_configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,13 @@
819819
"default": "localhost.key"
820820
}
821821
],
822+
"DD_SERVICE_EXTENSION_UDS_PATH": [
823+
{
824+
"implementation": "A",
825+
"type": "string",
826+
"default": null
827+
}
828+
],
822829
"DD_SERVICE_MAPPING": [
823830
{
824831
"implementation": "B",

0 commit comments

Comments
 (0)