Skip to content

Commit 4a8cdd7

Browse files
committed
Quic key log file support.
Motivation: Support Quic endpoint secrets dump in a keylog file, this is useful for debugging Quic or HTTP/3 Changes: Implement key logging Update server's client address validation to avoid sending retry packets if needed.
1 parent a7c53b1 commit 4a8cdd7

File tree

9 files changed

+238
-41
lines changed

9 files changed

+238
-41
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2011-2025 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.core.net;
12+
13+
/**
14+
* Quic client address validation on the server.
15+
*
16+
* @author <a href="mailto:[email protected]">Julien Viet</a>
17+
*/
18+
public enum QuicClientAddressValidation {
19+
20+
/**
21+
* The server won't perform any validation (no Retry packet is emitted).
22+
*/
23+
NONE,
24+
25+
/**
26+
* The server performs basic token validation without any crypto.
27+
*/
28+
BASIC,
29+
30+
/**
31+
* The server performs validation using cryptography.
32+
*/
33+
CRYPTO
34+
35+
}

vertx-core/src/main/java/io/vertx/core/net/QuicClientOptions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public QuicClientOptions setQLogConfig(QLogConfig qLogConfig) {
3232
return (QuicClientOptions) super.setQLogConfig(qLogConfig);
3333
}
3434

35+
@Override
36+
public QuicClientOptions setKeyLogFile(String keyLogFile) {
37+
return (QuicClientOptions) super.setKeyLogFile(keyLogFile);
38+
}
39+
3540
@Override
3641
public ClientSSLOptions getSslOptions() {
3742
return (ClientSSLOptions) super.getSslOptions();

vertx-core/src/main/java/io/vertx/core/net/QuicEndpointOptions.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public abstract class QuicEndpointOptions {
2424
private QuicOptions transportOptions;
2525
private SSLOptions sslOptions;
2626
private QLogConfig qLogConfig;
27+
private String keyLogFile;
2728

2829
public QuicEndpointOptions() {
2930
this.transportOptions = new QuicOptions();
@@ -36,6 +37,7 @@ public QuicEndpointOptions(QuicEndpointOptions other) {
3637
this.transportOptions = other.transportOptions.copy();
3738
this.sslOptions = other.sslOptions.copy();
3839
this.qLogConfig = qLogConfig != null ? new QLogConfig(qLogConfig) : null;
40+
this.keyLogFile = other.keyLogFile;
3941
}
4042

4143
public QuicEndpointOptions(JsonObject json) {
@@ -83,6 +85,31 @@ public QuicEndpointOptions setQLogConfig(QLogConfig qLogConfig) {
8385
return this;
8486
}
8587

88+
/**
89+
* @return the path of the configured key log file or {@code null} (default).
90+
*/
91+
public String getKeyLogFile() {
92+
return keyLogFile;
93+
}
94+
95+
/**
96+
* <p>Configures the endpoint to dump the cryptographic secrets using in TLS in the
97+
* <a href="https://www.ietf.org/archive/id/draft-thomson-tls-keylogfile-00.html">{@code SSLKEYLOGFILE}</a> format.</p>
98+
*
99+
* <p>The file might exist or will be created (in which case the parent file must exist), content will be appended
100+
* to the file.</p>
101+
*
102+
* <p>This should be used only for debugging purpose and must not be used in production. This feature is disabled
103+
* by default.</p>
104+
*
105+
* @param keyLogFile the path to the key log file
106+
* @return this exact object instance
107+
*/
108+
public QuicEndpointOptions setKeyLogFile(String keyLogFile) {
109+
this.keyLogFile = keyLogFile;
110+
return this;
111+
}
112+
86113
public JsonObject toJson() {
87114
throw new UnsupportedOperationException();
88115
}

vertx-core/src/main/java/io/vertx/core/net/QuicServerOptions.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import io.vertx.codegen.annotations.DataObject;
1414

1515
import java.time.Duration;
16+
import java.util.Objects;
1617

1718
/**
1819
* Config operations of a Quic server.
@@ -23,18 +24,18 @@
2324
public class QuicServerOptions extends QuicEndpointOptions {
2425

2526
public static final boolean DEFAULT_LOAD_BALANCED = false;
26-
public static final boolean DEFAULT_VALIDATE_CLIENT_ADDRESS = false;
27+
public static final QuicClientAddressValidation DEFAULT_CLIENT_ADDRESS_VALIDATION = QuicClientAddressValidation.BASIC;
2728
public static final KeyCertOptions DEFAULT_CLIENT_ADDRESS_VALIDATION_KEY = null;
2829
public static final Duration DEFAULT_CLIENT_ADDRESS_VALIDATION_TIME_WINDOW = Duration.ofSeconds(30);
2930

3031
private boolean loadBalanced;
31-
private boolean validateClientAddress;
32+
private QuicClientAddressValidation clientAddressValidation;
3233
private Duration clientAddressValidationTimeWindow;
3334
private KeyCertOptions clientAddressValidationKey;
3435

3536
public QuicServerOptions() {
3637
loadBalanced = DEFAULT_LOAD_BALANCED;
37-
validateClientAddress = DEFAULT_VALIDATE_CLIENT_ADDRESS;
38+
clientAddressValidation = DEFAULT_CLIENT_ADDRESS_VALIDATION;
3839
clientAddressValidationKey = DEFAULT_CLIENT_ADDRESS_VALIDATION_KEY;
3940
clientAddressValidationTimeWindow = DEFAULT_CLIENT_ADDRESS_VALIDATION_TIME_WINDOW;
4041
}
@@ -45,7 +46,7 @@ public QuicServerOptions(QuicServerOptions other) {
4546
KeyCertOptions tokenValidationKey = other.clientAddressValidationKey;
4647

4748
this.loadBalanced = other.loadBalanced;
48-
this.validateClientAddress = other.validateClientAddress;
49+
this.clientAddressValidation = other.clientAddressValidation;
4950
this.clientAddressValidationTimeWindow = other.clientAddressValidationTimeWindow;
5051
this.clientAddressValidationKey = tokenValidationKey != null ? tokenValidationKey.copy() : null;
5152
}
@@ -55,6 +56,11 @@ public QuicServerOptions setQLogConfig(QLogConfig qLogConfig) {
5556
return (QuicServerOptions) super.setQLogConfig(qLogConfig);
5657
}
5758

59+
@Override
60+
public QuicServerOptions setKeyLogFile(String keyLogFile) {
61+
return (QuicServerOptions) super.setKeyLogFile(keyLogFile);
62+
}
63+
5864
@Override
5965
public ServerSSLOptions getSslOptions() {
6066
return (ServerSSLOptions) super.getSslOptions();
@@ -87,8 +93,8 @@ public QuicServerOptions setLoadBalanced(boolean loadBalanced) {
8793
/**
8894
* @return whether the server performs address validation
8995
*/
90-
public boolean getValidateClientAddress() {
91-
return validateClientAddress;
96+
public QuicClientAddressValidation getClientAddressValidation() {
97+
return clientAddressValidation;
9298
}
9399

94100
/**
@@ -97,11 +103,11 @@ public boolean getValidateClientAddress() {
97103
*
98104
* <p>Client address validation requires you to also {@link #setClientAddressValidationKey(KeyCertOptions) set} a key for token signing/verification.</p>
99105
*
100-
* @param validateClientAddress whether to perform address validation
106+
* @param clientAddressValidation whether to perform address validation
101107
* @return this exact object instance
102108
*/
103-
public QuicServerOptions setValidateClientAddress(boolean validateClientAddress) {
104-
this.validateClientAddress = validateClientAddress;
109+
public QuicServerOptions setClientAddressValidation(QuicClientAddressValidation clientAddressValidation) {
110+
this.clientAddressValidation = Objects.requireNonNull(clientAddressValidation);
105111
return this;
106112
}
107113

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2011-2025 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.core.net.impl.quic;
12+
13+
import io.netty.handler.codec.quic.BoringSSLKeylog;
14+
15+
import javax.net.ssl.SSLEngine;
16+
import java.io.File;
17+
import java.io.FileWriter;
18+
import java.io.PrintWriter;
19+
import java.nio.charset.StandardCharsets;
20+
21+
/**
22+
* @author <a href="mailto:[email protected]">Julien Viet</a>
23+
*/
24+
class KeyLogFile implements BoringSSLKeylog {
25+
26+
private final File file;
27+
28+
KeyLogFile(File file) {
29+
this.file = file;
30+
}
31+
32+
@Override
33+
public void logKey(SSLEngine engine, String key) {
34+
try (PrintWriter out = new PrintWriter(new FileWriter(file, StandardCharsets.UTF_8, true))) {
35+
out.println(key);
36+
out.flush();
37+
} catch (Exception ignore) {
38+
}
39+
}
40+
}

vertx-core/src/main/java/io/vertx/core/net/impl/quic/QuicEndpointImpl.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.netty.channel.Channel;
1515
import io.netty.channel.ChannelFuture;
1616
import io.netty.channel.ChannelHandler;
17+
import io.netty.handler.codec.quic.BoringSSLKeylog;
1718
import io.netty.handler.codec.quic.FlushStrategy;
1819
import io.netty.handler.codec.quic.QuicCodecBuilder;
1920
import io.vertx.core.Completable;
@@ -37,7 +38,12 @@
3738
import io.vertx.core.spi.tls.QuicSslContextFactory;
3839
import io.vertx.core.spi.tls.SslContextFactory;
3940

41+
import java.io.File;
42+
import java.io.FileWriter;
43+
import java.io.IOException;
44+
import java.io.PrintWriter;
4045
import java.net.InetSocketAddress;
46+
import java.nio.charset.StandardCharsets;
4147
import java.time.Duration;
4248
import java.time.temporal.ChronoUnit;
4349
import java.util.EnumMap;
@@ -65,6 +71,36 @@ public abstract class QuicEndpointImpl implements QuicEndpointInternal, MetricsP
6571
private FlushStrategy flushStrategy;
6672

6773
public QuicEndpointImpl(VertxInternal vertx, QuicEndpointOptions options) {
74+
75+
String keyLogFilePath = options.getKeyLogFile();
76+
File keylogFile;
77+
if (keyLogFilePath != null) {
78+
keylogFile = new File(keyLogFilePath);
79+
File parent;
80+
if (keylogFile.exists() && keylogFile.isFile()) {
81+
if (!keylogFile.isFile()) {
82+
keylogFile = null;
83+
}
84+
} else if ((parent = keylogFile.getParentFile()).exists() && parent.isDirectory()) {
85+
try {
86+
if (!keylogFile.createNewFile()) {
87+
keylogFile = null;
88+
}
89+
} catch (IOException ignore) {
90+
keylogFile = null;
91+
}
92+
}
93+
} else {
94+
keylogFile = null;
95+
}
96+
97+
BoringSSLKeylog keylog;
98+
if (keylogFile != null) {
99+
keylog = new KeyLogFile(keylogFile);
100+
} else {
101+
keylog = null;
102+
}
103+
68104
this.options = options;
69105
this.vertx = vertx;
70106
this.manager = new SslContextManager(new SSLEngineOptions() {
@@ -74,7 +110,7 @@ public SSLEngineOptions copy() {
74110
}
75111
@Override
76112
public SslContextFactory sslContextFactory() {
77-
return new QuicSslContextFactory();
113+
return new QuicSslContextFactory(keylog);
78114
}
79115
});
80116
}

vertx-core/src/main/java/io/vertx/core/net/impl/quic/QuicServerImpl.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,20 @@ protected QuicCodecBuilder<?> codecBuilder(ContextInternal context, SslContextPr
120120
QuicSslContext sslContext = QuicSslContextBuilder.buildForServerWithSni(name -> (QuicSslContext) mapping.map(name));
121121
QuicTokenHandler qtc = tokenHandler;
122122
if (qtc == null) {
123-
if (options.getValidateClientAddress()) {
124-
KeyCertOptions tokenValidationKey = options.getClientAddressValidationKey();
125-
if (tokenValidationKey == null) {
126-
throw new IllegalArgumentException("The server must be configured with a token validation key to operate address validation");
127-
}
128-
Duration timeWindow = options.getClientAddressValidationTimeWindow();
129-
TokenManager tokenManager = new TokenManager(vertx, timeWindow);
130-
tokenManager.init(tokenValidationKey);
131-
qtc = tokenManager;
132-
} else {
133-
qtc = InsecureQuicTokenHandler.INSTANCE;
123+
switch (options.getClientAddressValidation()) {
124+
case BASIC:
125+
qtc = InsecureQuicTokenHandler.INSTANCE;
126+
break;
127+
case CRYPTO:
128+
KeyCertOptions tokenValidationKey = options.getClientAddressValidationKey();
129+
if (tokenValidationKey == null) {
130+
throw new IllegalArgumentException("The server must be configured with a token validation key to operate address validation");
131+
}
132+
Duration timeWindow = options.getClientAddressValidationTimeWindow();
133+
TokenManager tokenManager = new TokenManager(vertx, timeWindow);
134+
tokenManager.init(tokenValidationKey);
135+
qtc = tokenManager;
136+
break;
134137
}
135138
}
136139
QuicServerCodecBuilder builder = new QuicServerCodecBuilder().sslContext(sslContext)

vertx-core/src/main/java/io/vertx/core/spi/tls/QuicSslContextFactory.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
package io.vertx.core.spi.tls;
1313

14+
import io.netty.handler.codec.quic.BoringSSLKeylog;
1415
import io.netty.handler.codec.quic.QuicSslContextBuilder;
1516
import io.netty.handler.ssl.ApplicationProtocolConfig;
1617
import io.netty.handler.ssl.ClientAuth;
@@ -21,10 +22,8 @@
2122
import io.netty.handler.ssl.SslContextBuilder;
2223
import io.netty.handler.ssl.SslProvider;
2324

24-
import javax.net.ssl.KeyManagerFactory;
25-
import javax.net.ssl.SSLException;
26-
import javax.net.ssl.SSLSessionContext;
27-
import javax.net.ssl.TrustManagerFactory;
25+
import javax.net.ssl.*;
26+
import java.io.File;
2827
import java.util.Collection;
2928
import java.util.List;
3029
import java.util.Set;
@@ -37,9 +36,6 @@
3736
*/
3837
public class QuicSslContextFactory implements SslContextFactory {
3938

40-
public QuicSslContextFactory() {
41-
}
42-
4339
private boolean forServer;
4440
private boolean forClient;
4541
private Set<String> enabledProtocols;
@@ -51,6 +47,11 @@ public QuicSslContextFactory() {
5147
private ClientAuth clientAuth;
5248
private KeyManagerFactory kmf;
5349
private TrustManagerFactory tmf;
50+
private final BoringSSLKeylog keylog;
51+
52+
public QuicSslContextFactory(BoringSSLKeylog keylog) {
53+
this.keylog = keylog;
54+
}
5455

5556
@Override
5657
public SslContextFactory useAlpn(boolean useAlpn) {
@@ -121,6 +122,7 @@ private SslContext createContext(boolean client, KeyManagerFactory kmf, TrustMan
121122
builder.clientAuth(clientAuth);
122123
}
123124
}
125+
builder.keylog(keylog);
124126
/*
125127
Collection<String> cipherSuites = enabledCipherSuites;
126128
switch (sslProvider) {

0 commit comments

Comments
 (0)