Skip to content

Commit 28b56de

Browse files
authored
Merge pull request quarkusio#55350 from cescoffier/quarkus4/grpc-domain-socket
gRPC - Add Unix domain socket support for server and client
2 parents 7796592 + f797022 commit 28b56de

13 files changed

Lines changed: 413 additions & 54 deletions

File tree

.github/native-tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
{
142142
"category": "gRPC",
143143
"timeout": 80,
144-
"test-modules": "grpc-health, grpc-encoding, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-streaming-native, grpc-tls, grpc-tls-p12, grpc-test-random-port",
144+
"test-modules": "grpc-domain-socket, grpc-health, grpc-encoding, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-streaming-native, grpc-tls, grpc-tls-p12, grpc-test-random-port",
145145
"os-name": "ubuntu-latest"
146146
},
147147
{

docs/src/main/asciidoc/grpc-service-consumption.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,34 @@ Don't forget to replace it with the name you used in the `@GrpcClient` annotatio
217217

218218
IMPORTANT: When you enable `quarkus.grpc.clients."client-name".xds.enabled`, it's the xDS that should handle most of the configuration above.
219219

220+
=== Domain Socket
221+
222+
To connect a gRPC client to a server listening on a Unix domain socket, set the `domain-socket` property to the socket path:
223+
224+
[source,properties]
225+
----
226+
quarkus.grpc.clients.hello.domain-socket=/var/run/grpc.sock
227+
----
228+
229+
When `domain-socket` is set, the `host` and `port` properties are ignored. The client connects directly to the specified socket file.
230+
231+
Here is a full example with both the server and client configured to use the same domain socket:
232+
233+
[source,properties]
234+
----
235+
# Server: listen on domain socket only
236+
quarkus.http.domain-socket-enabled=true
237+
quarkus.http.domain-socket=/var/run/grpc.sock
238+
quarkus.http.host-enabled=false
239+
240+
# Client: connect via domain socket
241+
quarkus.grpc.clients.hello.domain-socket=/var/run/grpc.sock
242+
----
243+
244+
IMPORTANT: The Stork name resolver is incompatible with domain sockets. Setting both `domain-socket` and `name-resolver=stork` will result in a startup error.
245+
246+
NOTE: Unix domain sockets require JDK 16+ and are not available on Windows.
247+
220248
=== Custom Channel building
221249

222250
When Quarkus builds a gRPC Channel instance (the way gRPC clients communicate with gRPC services on a lower network level), users can apply their own Channel(Builder) customizers. The customizers are applied by `priority`, the higher the number the later customizer is applied. The customizers are applied before Quarkus applies user's client configuration; e.g. ideal for some initial defaults per all clients.

docs/src/main/asciidoc/grpc-service-implementation.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,27 @@ public interface ServerBuilderCustomizer<T extends ServerBuilder<T>> {
294294
}
295295
----
296296

297+
=== Domain Socket
298+
299+
The gRPC server uses the Quarkus HTTP server, so you can configure it to listen on a Unix domain socket using the standard HTTP configuration properties:
300+
301+
[source,properties]
302+
----
303+
quarkus.http.domain-socket-enabled=true
304+
quarkus.http.domain-socket=/var/run/grpc.sock
305+
----
306+
307+
When a domain socket is enabled, the gRPC server automatically accepts gRPC requests on it — no gRPC-specific configuration is required.
308+
309+
To listen *only* on the domain socket (disabling TCP), set:
310+
311+
[source,properties]
312+
----
313+
quarkus.http.host-enabled=false
314+
----
315+
316+
NOTE: Unix domain sockets require JDK 16+ and are not available on Windows. Native transport (`quarkus.vertx.native-transport`) is not required.
317+
297318
== Server Interceptors
298319

299320
gRPC server interceptors let you perform logic, such as authentication, before your service is invoked.

extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/config/GrpcClientConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ public interface GrpcClientConfiguration {
3636
@WithDefault("localhost")
3737
String host();
3838

39+
/**
40+
* The path to a Unix domain socket. When set, the client connects to the gRPC server
41+
* using a Unix domain socket instead of a TCP connection. The {@code host} and {@code port}
42+
* properties are ignored in this case.
43+
* <p>
44+
* Unix domain sockets are not available on Windows.
45+
*/
46+
Optional<String> domainSocket();
47+
3948
/**
4049
* The name of the TLS configuration to use.
4150
* <p>

extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/Channels.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ public static Channel createChannel(String name, Set<String> perClientIntercepto
115115
String nameResolver = clientConfig.nameResolver();
116116

117117
boolean stork = Stork.STORK.equalsIgnoreCase(nameResolver);
118+
boolean useDomainSocket = clientConfig.domainSocket().isPresent();
119+
if (useDomainSocket && stork) {
120+
throw new IllegalArgumentException(
121+
"gRPC client '" + name + "' cannot use both domain-socket and Stork name resolver");
122+
}
118123

119124
String[] resolverSplit = nameResolver.split(":");
120125
String resolver = resolverSplit[0];
@@ -217,13 +222,22 @@ public static Channel createChannel(String name, Set<String> perClientIntercepto
217222
options);
218223

219224
Channel channel;
225+
String authority;
220226
if (stork) {
221227
ManagedExecutor executor = container.instance(ManagedExecutor.class).get();
222-
channel = new StorkGrpcChannel(client, clientConfig.host(), clientConfig.stork(), executor); // host = service-name
228+
channel = new StorkGrpcChannel(client, clientConfig.host(), clientConfig.stork(), executor);
229+
authority = host + ":" + port;
230+
LOGGER.debugf("Target for client '%s': %s (stork)", name, host);
231+
} else if (useDomainSocket) {
232+
String socketPath = clientConfig.domainSocket().get();
233+
authority = "localhost";
234+
channel = new GrpcIoClientChannel(client, new DomainSocketAddress(socketPath));
235+
LOGGER.debugf("Target for client '%s': unix:%s", name, socketPath);
223236
} else {
224237
channel = new GrpcIoClientChannel(client, SocketAddress.inetSocketAddress(port, host));
238+
authority = host + ":" + port;
239+
LOGGER.debugf("Target for client '%s': %s", name, authority);
225240
}
226-
LOGGER.debugf("Target for client '%s': %s", name, host + ":" + port);
227241

228242
List<ClientInterceptor> interceptors = new ArrayList<>();
229243
interceptors.addAll(interceptorContainer.getSortedPerServiceInterceptors(perClientInterceptors));
@@ -232,7 +246,7 @@ public static Channel createChannel(String name, Set<String> perClientIntercepto
232246
LOGGER.debug("Creating Vert.x gRPC channel ...");
233247

234248
return new InternalGrpcChannel(client, channel, ClientInterceptors.intercept(channel, interceptors),
235-
host + ":" + port);
249+
authority);
236250

237251
}
238252

@@ -331,4 +345,47 @@ public String authority() {
331345
return authority;
332346
}
333347
}
348+
349+
/**
350+
* gRPC over HTTP/2 requires the {@code :authority} pseudo-header, which Vert.x derives from
351+
* {@link SocketAddress#host()}. The standard {@code SocketAddress.domainSocketAddress()} returns
352+
* {@code null} for host, so we provide {@code "localhost"} to satisfy the gRPC protocol requirement.
353+
*/
354+
private record DomainSocketAddress(String path) implements SocketAddress {
355+
356+
@Override
357+
public String host() {
358+
return "localhost";
359+
}
360+
361+
@Override
362+
public String hostName() {
363+
return "localhost";
364+
}
365+
366+
@Override
367+
public int port() {
368+
return 0;
369+
}
370+
371+
@Override
372+
public boolean isDomainSocket() {
373+
return true;
374+
}
375+
376+
@Override
377+
public boolean isInetSocket() {
378+
return false;
379+
}
380+
381+
@Override
382+
public String hostAddress() {
383+
return null;
384+
}
385+
386+
@Override
387+
public String toString() {
388+
return path;
389+
}
390+
}
334391
}

extensions/spiffe-client/runtime/src/main/java/io/quarkus/spiffe/client/runtime/internal/SpiffeClientImpl.java

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ private static SocketAddress toSocketAddress(URI uri) {
221221
throw new ConfigurationException(
222222
"The SPIFFE client extension does not support unix scheme on Windows, use tcp:// instead.");
223223
}
224-
return new DomainSocketAddressWithAuthority(SocketAddress.domainSocketAddress(uri.getPath()));
224+
return SocketAddress.domainSocketAddress(uri.getPath());
225225
}
226226
return SocketAddress.inetSocketAddress(uri.getPort(), uri.getHost());
227227
}
@@ -237,53 +237,4 @@ private static void validateAudience(String audience) {
237237
throw new IllegalArgumentException("Audience must not contain spaces: '" + audience + "'");
238238
}
239239
}
240-
241-
/**
242-
* Works around <a href="https://github.com/eclipse-vertx/vert.x/issues/6220">Vert.x #6220 issue</a>.
243-
* Setting invalid host is enough to avoid validation failure while still using the domain socket.
244-
* TODO: drop this when Quarkus moves to Vert.x 5.1.4, however we will still need it for Quarkus 3.x
245-
*/
246-
private record DomainSocketAddressWithAuthority(SocketAddress delegate) implements SocketAddress {
247-
248-
@Override
249-
public String host() {
250-
// https://www.rfc-editor.org/rfc/rfc2606.html#section-2 says '.invalid' is reserved for invalid domain names
251-
return "uds.invalid";
252-
}
253-
254-
@Override
255-
public int port() {
256-
return 0;
257-
}
258-
259-
@Override
260-
public String path() {
261-
return delegate.path();
262-
}
263-
264-
@Override
265-
public String hostName() {
266-
return delegate.hostName();
267-
}
268-
269-
@Override
270-
public String hostAddress() {
271-
return delegate.hostAddress();
272-
}
273-
274-
@Override
275-
public boolean isInetSocket() {
276-
return delegate.isInetSocket();
277-
}
278-
279-
@Override
280-
public boolean isDomainSocket() {
281-
return delegate.isDomainSocket();
282-
}
283-
284-
@Override
285-
public String toString() {
286-
return delegate.toString();
287-
}
288-
}
289240
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>quarkus-integration-tests-parent</artifactId>
9+
<groupId>io.quarkus</groupId>
10+
<version>999-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>quarkus-integration-test-grpc-domain-socket</artifactId>
14+
<name>Quarkus - Integration Tests - gRPC - Domain Socket</name>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>io.quarkus</groupId>
19+
<artifactId>quarkus-grpc</artifactId>
20+
</dependency>
21+
<dependency>
22+
<groupId>io.quarkus</groupId>
23+
<artifactId>quarkus-arc</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>io.quarkus</groupId>
27+
<artifactId>quarkus-junit</artifactId>
28+
<scope>test</scope>
29+
</dependency>
30+
<dependency>
31+
<groupId>io.quarkus</groupId>
32+
<artifactId>quarkus-test-grpc</artifactId>
33+
<version>${project.version}</version>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.assertj</groupId>
38+
<artifactId>assertj-core</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
42+
<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
43+
<dependency>
44+
<groupId>io.quarkus</groupId>
45+
<artifactId>quarkus-grpc-deployment</artifactId>
46+
<version>${project.version}</version>
47+
<type>pom</type>
48+
<scope>test</scope>
49+
<exclusions>
50+
<exclusion>
51+
<groupId>*</groupId>
52+
<artifactId>*</artifactId>
53+
</exclusion>
54+
</exclusions>
55+
</dependency>
56+
<dependency>
57+
<groupId>io.quarkus</groupId>
58+
<artifactId>quarkus-arc-deployment</artifactId>
59+
<version>${project.version}</version>
60+
<type>pom</type>
61+
<scope>test</scope>
62+
<exclusions>
63+
<exclusion>
64+
<groupId>*</groupId>
65+
<artifactId>*</artifactId>
66+
</exclusion>
67+
</exclusions>
68+
</dependency>
69+
</dependencies>
70+
71+
<build>
72+
<plugins>
73+
<plugin>
74+
<groupId>io.quarkus</groupId>
75+
<artifactId>quarkus-maven-plugin</artifactId>
76+
<executions>
77+
<execution>
78+
<goals>
79+
<goal>generate-code</goal>
80+
<goal>build</goal>
81+
</goals>
82+
</execution>
83+
</executions>
84+
</plugin>
85+
</plugins>
86+
</build>
87+
88+
</project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.quarkus.it.grpc.uds;
2+
3+
import io.quarkus.grpc.GrpcService;
4+
import io.smallrye.mutiny.Uni;
5+
6+
@GrpcService
7+
public class HelloWorldService extends MutinyGreeterGrpc.GreeterImplBase {
8+
9+
@Override
10+
public Uni<HelloReply> sayHello(HelloRequest request) {
11+
String name = request.getName();
12+
return Uni.createFrom().item(
13+
HelloReply.newBuilder().setMessage("Hello " + name).build());
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
syntax = "proto3";
2+
3+
option java_multiple_files = true;
4+
option java_package = "io.quarkus.it.grpc.uds";
5+
6+
package helloworld;
7+
8+
service Greeter {
9+
rpc SayHello (HelloRequest) returns (HelloReply) {}
10+
}
11+
12+
message HelloRequest {
13+
string name = 1;
14+
}
15+
16+
message HelloReply {
17+
string message = 1;
18+
}

0 commit comments

Comments
 (0)