Skip to content

Commit 4cf0c72

Browse files
nikagraclaude
andcommitted
feat: enable shard-awareness with Proxy Protocol v2 in PrivateLink mode
Adds `proxyProtocol` support to `ClientRoutesConfig` so users can declare that the NLB uses Proxy Protocol v2 (PP2) when forwarding connections to ScyllaDB. With PP2, the NLB prepends a binary header carrying the original client source IP and port to each connection it opens to ScyllaDB; ScyllaDB reads the header and routes using the original source port, restoring shard-aware routing end-to-end through the NLB. Changes: - `ClientRoutesConfig`: add `proxyProtocol` field + `withProxyProtocol()` builder method; update equals/hashCode/toString - `DefaultDriverOption`: add `CLIENT_ROUTES_PROXY_PROTOCOL` enum constant - `TypedDriverOption` / `OptionsMap`: add typed wrapper and default value - `reference.conf`: add `advanced.client-routes.proxy-protocol = false` - `DefaultDriverContext`: parse `proxy-protocol` from HOCON; warn (not fail) when `proxyProtocol=true` but shard awareness is disabled - `TcpProxy` / `RoundRobinProxy`: add PP2 header injection mode — after connecting to the target, write the 28-byte TCP4 PP2 binary header carrying the original client IP and source port - `NlbSimulator`: add `proxyProtocol` constructor parameter; pass flag to both per-node (`TcpProxy`) and discovery (`RoundRobinProxy`) proxies - `ClientRoutesIT`: add `should_use_shard_awareness_through_pp2_nlb` test - `PRIVATELINK.md`: document the PP2 mechanism, header format, configuration, and infrastructure requirements Jira ID: DRIVER-391 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3b9f989 commit 4cf0c72

File tree

10 files changed

+343
-9
lines changed

10 files changed

+343
-9
lines changed

core/src/main/java/com/datastax/oss/driver/api/core/config/ClientRoutesConfig.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ public final class ClientRoutesConfig {
7373

7474
private final List<ClientRouteProxy> endpoints;
7575
private final String tableName;
76+
private final boolean proxyProtocol;
7677

77-
private ClientRoutesConfig(List<ClientRouteProxy> endpoints, String tableName) {
78+
private ClientRoutesConfig(
79+
List<ClientRouteProxy> endpoints, String tableName, boolean proxyProtocol) {
7880
if (endpoints == null || endpoints.isEmpty()) {
7981
throw new IllegalArgumentException("At least one endpoint must be specified");
8082
}
@@ -86,6 +88,7 @@ private ClientRoutesConfig(List<ClientRouteProxy> endpoints, String tableName) {
8688
}
8789
this.endpoints = Collections.unmodifiableList(new ArrayList<>(endpoints));
8890
this.tableName = tableName;
91+
this.proxyProtocol = proxyProtocol;
8992
}
9093

9194
/**
@@ -108,6 +111,21 @@ public String getTableName() {
108111
return tableName;
109112
}
110113

114+
/**
115+
* Returns whether Proxy Protocol v2 is in use between the NLB and ScyllaDB nodes.
116+
*
117+
* <p>When {@code true}, the driver assumes the NLB prepends a PP2 binary header to each
118+
* connection it opens to ScyllaDB. The header carries the original client IP and source port,
119+
* which ScyllaDB uses for shard-aware routing. This restores shard-awareness end-to-end through
120+
* the NLB. Requires both the NLB and ScyllaDB to be configured for Proxy Protocol v2, and {@code
121+
* advanced-shard-awareness.enabled} must be {@code true} (the default).
122+
*
123+
* @return {@code true} if Proxy Protocol v2 is enabled.
124+
*/
125+
public boolean isProxyProtocol() {
126+
return proxyProtocol;
127+
}
128+
111129
/**
112130
* Creates a new builder for constructing a {@link ClientRoutesConfig}.
113131
*
@@ -127,12 +145,14 @@ public boolean equals(Object o) {
127145
return false;
128146
}
129147
ClientRoutesConfig that = (ClientRoutesConfig) o;
130-
return endpoints.equals(that.endpoints) && tableName.equals(that.tableName);
148+
return proxyProtocol == that.proxyProtocol
149+
&& endpoints.equals(that.endpoints)
150+
&& tableName.equals(that.tableName);
131151
}
132152

133153
@Override
134154
public int hashCode() {
135-
return Objects.hash(endpoints, tableName);
155+
return Objects.hash(endpoints, tableName, proxyProtocol);
136156
}
137157

138158
@Override
@@ -143,6 +163,8 @@ public String toString() {
143163
+ ", tableName='"
144164
+ tableName
145165
+ '\''
166+
+ ", proxyProtocol="
167+
+ proxyProtocol
146168
+ '}';
147169
}
148170

@@ -151,6 +173,7 @@ public static final class Builder {
151173
private final List<ClientRouteProxy> endpoints = new ArrayList<>();
152174
private final Set<String> seenConnectionIds = new HashSet<>();
153175
private String tableName = DEFAULT_TABLE_NAME;
176+
private boolean proxyProtocol = false;
154177

155178
/**
156179
* Adds an endpoint to the configuration.
@@ -208,6 +231,26 @@ Builder withTableName(@NonNull String tableName) {
208231
return this;
209232
}
210233

234+
/**
235+
* Sets whether Proxy Protocol v2 (PP2) is in use between the NLB and ScyllaDB nodes.
236+
*
237+
* <p>When {@code true}, the driver assumes the NLB prepends a PP2 binary header to each
238+
* connection it opens to ScyllaDB. The header carries the original client source IP and port,
239+
* which ScyllaDB uses to route the connection to the correct shard. This restores
240+
* shard-awareness end-to-end through the NLB.
241+
*
242+
* <p>Requires: (1) the NLB configured with PP2, (2) ScyllaDB configured to accept PP2, (3)
243+
* {@code advanced-shard-awareness.enabled = true} (the default).
244+
*
245+
* @param proxyProtocol {@code true} to enable PP2 mode.
246+
* @return this builder.
247+
*/
248+
@NonNull
249+
public Builder withProxyProtocol(boolean proxyProtocol) {
250+
this.proxyProtocol = proxyProtocol;
251+
return this;
252+
}
253+
211254
/**
212255
* Builds the {@link ClientRoutesConfig} with the configured endpoints and table name.
213256
*
@@ -216,7 +259,7 @@ Builder withTableName(@NonNull String tableName) {
216259
*/
217260
@NonNull
218261
public ClientRoutesConfig build() {
219-
return new ClientRoutesConfig(endpoints, tableName);
262+
return new ClientRoutesConfig(endpoints, tableName, proxyProtocol);
220263
}
221264
}
222265
}

core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,19 @@ public enum DefaultDriverOption implements DriverOption {
10971097
*
10981098
* <p>Value type: list of HOCON objects
10991099
*/
1100-
CLIENT_ROUTES_ENDPOINTS("advanced.client-routes.endpoints");
1100+
CLIENT_ROUTES_ENDPOINTS("advanced.client-routes.endpoints"),
1101+
/**
1102+
* Whether Proxy Protocol v2 (PP2) is in use between the NLB and ScyllaDB nodes.
1103+
*
1104+
* <p>When {@code true}, the driver assumes the NLB prepends a PP2 binary header to each
1105+
* connection it opens to ScyllaDB. The header carries the original client source IP and port,
1106+
* enabling shard-aware routing through the NLB. Requires: (1) NLB configured with PP2, (2)
1107+
* ScyllaDB configured to accept PP2, (3) {@code advanced-shard-awareness.enabled = true} (the
1108+
* default).
1109+
*
1110+
* <p>Value type: boolean
1111+
*/
1112+
CLIENT_ROUTES_PROXY_PROTOCOL("advanced.client-routes.proxy-protocol");
11011113

11021114
private final String path;
11031115

core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ protected static void fillWithDriverDefaults(OptionsMap map) {
398398
"PRESERVE_REPLICA_ORDER");
399399
// CLIENT_ROUTES_ENDPOINTS is intentionally omitted: it is a list-of-objects (compound HOCON
400400
// values) with no sensible scalar default, analogous to how CONFIG_RELOAD_INTERVAL is omitted.
401+
map.put(TypedDriverOption.CLIENT_ROUTES_PROXY_PROTOCOL, false);
401402
}
402403

403404
@Immutable

core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,11 @@ public String toString() {
944944
// DriverExecutionProfile API); it is excluded from the TypedDriverOptionTest consistency check
945945
// accordingly.
946946

947+
/** Whether Proxy Protocol v2 is in use between the NLB and ScyllaDB nodes. */
948+
public static final TypedDriverOption<Boolean> CLIENT_ROUTES_PROXY_PROTOCOL =
949+
new TypedDriverOption<>(
950+
DefaultDriverOption.CLIENT_ROUTES_PROXY_PROTOCOL, GenericType.BOOLEAN);
951+
947952
private static Iterable<TypedDriverOption<?>> introspectBuiltInValues() {
948953
try {
949954
ImmutableList.Builder<TypedDriverOption<?>> result = ImmutableList.builder();

core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,11 @@ ClientRoutesConfig buildClientRoutesConfigFromFile() {
514514

515515
ClientRoutesConfig.Builder builder = ClientRoutesConfig.builder();
516516

517+
if (defaultProfile.isDefined(DefaultDriverOption.CLIENT_ROUTES_PROXY_PROTOCOL)) {
518+
builder.withProxyProtocol(
519+
defaultProfile.getBoolean(DefaultDriverOption.CLIENT_ROUTES_PROXY_PROTOCOL));
520+
}
521+
517522
for (int i = 0; i < endpointsList.size(); i++) {
518523
if (!(endpointsList.get(i) instanceof ConfigObject)) {
519524
throw new IllegalArgumentException(
@@ -644,6 +649,29 @@ private void validateClientRoutesConfiguration(ClientRoutesConfig clientRoutesCo
644649
+ "They are mutually exclusive. Please use either a secure connect bundle OR "
645650
+ "client routes configuration, but not both.");
646651
}
652+
if (clientRoutesConfig.isProxyProtocol()) {
653+
boolean shardAwarenessEnabled =
654+
getConfig()
655+
.getDefaultProfile()
656+
.getBoolean(DefaultDriverOption.CONNECTION_ADVANCED_SHARD_AWARENESS_ENABLED);
657+
if (!shardAwarenessEnabled) {
658+
// proxyProtocol=true signals that the NLB will forward the driver's original source port
659+
// to ScyllaDB via a PP2 header, enabling shard-aware connection routing through the proxy.
660+
// However, this only has effect when advanced shard awareness is also enabled — that is the
661+
// mechanism that binds a shard-specific local port in the first place. With shard awareness
662+
// disabled the driver uses a random local port, so PP2 forwards a port that carries no
663+
// shard intent. The setting is therefore ignored. If shard-aware routing through the NLB
664+
// is desired, enable advanced-shard-awareness (it is on by default).
665+
LOG.warn(
666+
"[{}] ClientRoutesConfig has proxyProtocol=true but {} is false. "
667+
+ "Proxy Protocol v2 has no effect without advanced shard awareness — "
668+
+ "the proxyProtocol flag will be ignored. To enable shard-aware routing "
669+
+ "through the NLB, set advanced-shard-awareness.enabled=true (the default).",
670+
getSessionName(),
671+
DefaultDriverOption.CONNECTION_ADVANCED_SHARD_AWARENESS_ENABLED.getPath());
672+
}
673+
}
674+
647675
DriverExecutionProfile defaultProfile = getConfig().getDefaultProfile();
648676
if (defaultProfile.isDefined(DefaultDriverOption.ADDRESS_TRANSLATOR_CLASS)) {
649677
String className = defaultProfile.getString(DefaultDriverOption.ADDRESS_TRANSLATOR_CLASS);

core/src/main/resources/reference.conf

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,25 @@ datastax-java-driver {
11461146
# Required: yes (if client routes are to be used)
11471147
# endpoints = []
11481148

1149+
# Whether Proxy Protocol v2 (PP2) is in use between the NLB and ScyllaDB nodes.
1150+
#
1151+
# When true, the driver assumes the NLB prepends a PP2 binary header to each connection it opens
1152+
# to ScyllaDB. The header carries the original client source IP and port, which ScyllaDB uses to
1153+
# route the connection to the correct shard. This restores shard-awareness end-to-end through the
1154+
# NLB. The driver's own role is to continue binding shard-specific local ports as usual
1155+
# (controlled by advanced-shard-awareness.enabled, which is true by default); the NLB then
1156+
# forwards those ports to ScyllaDB via the PP2 header.
1157+
#
1158+
# Requires: (1) NLB configured with PP2, (2) ScyllaDB configured to accept PP2,
1159+
# (3) advanced.connection.advanced-shard-awareness.enabled = true (the default).
1160+
#
1161+
# If this is true but advanced-shard-awareness.enabled is false, the driver logs a warning and
1162+
# ignores the setting — there is no shard-specific port to forward.
1163+
#
1164+
# Required: no
1165+
# Default: false
1166+
proxy-protocol = false
1167+
11491168
}
11501169

11511170
# Whether to resolve the addresses passed to `basic.contact-points`.

integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/ClientRoutesIT.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,59 @@ public void should_select_tls_port_when_ssl_configured() throws Exception {
820820
}
821821
}
822822

823+
@Test
824+
public void should_use_shard_awareness_through_pp2_nlb() throws Exception {
825+
// Verify server supports client_routes
826+
try (CqlSession admin = openAdminSession()) {
827+
requireSystemClientRoutesTable(admin);
828+
}
829+
try (CcmBridge ccm = CcmBridge.builder().withNodes(1).build()) {
830+
ccm.create();
831+
ccm.start();
832+
833+
// Create PP2-enabled NLB
834+
NlbSimulator nlb =
835+
new NlbSimulator(ccm, NLB_ADDRESS, NLB_BASE_PORT, /* proxyProtocol= */ true);
836+
try {
837+
nlb.addNode(1);
838+
Map<Integer, UUID> hostIds = collectHostIds(ccm, 1);
839+
postClientRoutes(ccm, hostIds, nlb);
840+
waitForRoutesVisibleOnAllNodes(ccm, hostIds.keySet(), hostIds.size());
841+
842+
ClientRoutesConfig config =
843+
ClientRoutesConfig.builder()
844+
.addEndpoint(new ClientRouteProxy(CONNECTION_ID, NLB_ADDRESS))
845+
.withProxyProtocol(true)
846+
.build();
847+
848+
try (CqlSession session = openNlbSession(config, nlb)) {
849+
assertQueryWorks(session);
850+
851+
// With PP2, the NLB forwards the driver's original source port to ScyllaDB.
852+
// The driver binds shard-specific local ports (advanced shard awareness, on by default),
853+
// so ScyllaDB can route each connection to the correct shard. Verify the pool works
854+
// correctly by running queries and waiting for the full pool to be established.
855+
Node node = session.getMetadata().getNodes().values().iterator().next();
856+
int shardCount =
857+
node.getShardingInfo() != null ? node.getShardingInfo().getShardsCount() : 1;
858+
859+
// Wait for full shard-aware pool to be established (one connection per shard)
860+
await()
861+
.atMost(30, TimeUnit.SECONDS)
862+
.pollInterval(1, TimeUnit.SECONDS)
863+
.until(() -> node.getOpenConnections() >= shardCount);
864+
865+
// Run queries to verify shard-aware routing works end-to-end through the PP2 NLB
866+
for (int i = 0; i < 20; i++) {
867+
assertQueryWorks(session);
868+
}
869+
}
870+
} finally {
871+
nlb.close();
872+
}
873+
}
874+
}
875+
823876
@Test
824877
public void should_work_with_mixed_proxy_and_direct_nodes() throws Exception {
825878
try (CqlSession admin = openAdminSession()) {

integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/NlbSimulator.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class NlbSimulator implements Closeable {
5757
private final CcmBridge ccmBridge;
5858
private final String bindAddress;
5959
private final int basePort;
60+
private final boolean proxyProtocol;
6061

6162
// Discovery port proxy: round-robins across all nodes
6263
private volatile RoundRobinProxy discoveryProxy;
@@ -74,9 +75,24 @@ public class NlbSimulator implements Closeable {
7475
* @param basePort the base port for NLB (discovery on basePort, per-node on basePort+nodeId)
7576
*/
7677
public NlbSimulator(CcmBridge ccmBridge, String bindAddress, int basePort) {
78+
this(ccmBridge, bindAddress, basePort, false);
79+
}
80+
81+
/**
82+
* Creates an NLB simulator with optional Proxy Protocol v2 support.
83+
*
84+
* @param ccmBridge the CCM bridge to get node addresses from
85+
* @param bindAddress the IP address to bind proxy listeners on (e.g. "127.254.254.254")
86+
* @param basePort the base port for NLB (discovery on basePort, per-node on basePort+nodeId)
87+
* @param proxyProtocol when {@code true}, all proxied connections will include a PP2 binary
88+
* header carrying the original client IP and source port
89+
*/
90+
public NlbSimulator(
91+
CcmBridge ccmBridge, String bindAddress, int basePort, boolean proxyProtocol) {
7792
this.ccmBridge = ccmBridge;
7893
this.bindAddress = bindAddress;
7994
this.basePort = basePort;
95+
this.proxyProtocol = proxyProtocol;
8096
}
8197

8298
/** Returns the bind address used by this NLB simulator. */
@@ -114,7 +130,7 @@ public synchronized void addNode(int nodeId) throws IOException {
114130

115131
// Create per-node proxy first, before updating state
116132
int nodePort = getNodePort(nodeId);
117-
TcpProxy proxy = new TcpProxy(bindAddress, nodePort, nodeAddr);
133+
TcpProxy proxy = new TcpProxy(bindAddress, nodePort, nodeAddr, proxyProtocol);
118134

119135
// Update state and rebuild discovery proxy; if rebuild fails, close the new proxy
120136
activeNodes.add(nodeId);
@@ -164,7 +180,7 @@ private void rebuildDiscoveryProxy() throws IOException {
164180
}
165181

166182
// Create new proxy before closing old one to avoid inconsistent state on failure
167-
discoveryProxy = new RoundRobinProxy(bindAddress, basePort, targets);
183+
discoveryProxy = new RoundRobinProxy(bindAddress, basePort, targets, proxyProtocol);
168184
if (oldProxy != null) {
169185
oldProxy.close();
170186
}

0 commit comments

Comments
 (0)