Skip to content

Commit 45afe24

Browse files
authored
fix: Handle HAProxy protocol in TLS pipeline (kroxylicious#3546)
Adds support for HAProxy PROXY protocol when TLS is enabled. Resolves kroxylicious#287 - used CompositeByteBuf and Unmodifiable copy for tlvs - HaProxyMessageHandler: extend SimpleChannelInboundHandler<HAProxyMessage> so the ref-counted message is auto-released after processing. - ProxyChannelStateMachine#onClientActive: drop the always-true instanceof guard and reuse the ClientActive instance. - con-configuration-outline.adoc: fix YAML example to use `mode: disabled` instead of the non-existent `enabled` field. - HaProxyMessageHandlerTest: add test asserting refCnt == 0 after read. Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Hrishabh Gupta <hgupta@confluent.io>
1 parent 827130a commit 45afe24

34 files changed

Lines changed: 1567 additions & 292 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependency-reduced-pom.xml
2828
.idea/kubernetes-settings.xml
2929
.idea/sonarlint.xml
3030
.idea/copilot.*
31+
.idea/go.imports.xml
3132

3233
# AWS User-specific
3334
.idea/**/aws.xml
@@ -110,3 +111,4 @@ docs/*.html
110111
.claude/agent-memory
111112
.op/
112113
.claude/projects/
114+
.claude/worktrees

kroxylicious-docs/docs/_assemblies/assembly-configuring-proxy.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ include::../_modules/configuring/con-configuring-vc-other-settings.adoc[leveloff
2525
include::../_modules/configuring/con-configuring-toplevel-other-settings.adoc[leveloffset=+1]
2626
include::../_modules/configuring/con-configuring-network-settings.adoc[leveloffset=+1]
2727
include::../_modules/configuring/con-configuring-idle-timeouts.adoc[leveloffset=+1]
28+
include::../_modules/configuring/con-configuring-proxy-protocol.adoc[leveloffset=+1]
2829
2930
include::../_modules/configuring/ref-configuring-proxy-example.adoc[leveloffset=+1]

kroxylicious-docs/docs/_modules/configuring/con-configuration-outline.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ virtualClusters:
3333
# ...
3434
subjectBuilder:
3535
# ...
36+
proxyProtocol: # <10>
37+
mode: disabled
3638
3739
# ...
3840
----
@@ -50,3 +52,4 @@ where:
5052
** `targetCluster` defines the Kafka cluster that the virtual cluster (`my-cluster-proxy`) proxies.
5153
** `gateways` defines the gateways configuration for this virtual cluster.
5254
** `subjectBuilder` (Optional) defines the transport subject builder configuration.
55+
** `proxyProtocol` (Optional) Enable HAProxy PROXY protocol decoding when deployed behind a PROXY protocol-capable load balancer.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
////
2+
// Copyright Kroxylicious Authors.
3+
//
4+
// Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
////
6+
7+
:_mod-docs-content-type: CONCEPT
8+
9+
[id='con-configuring-proxy-protocol-{context}']
10+
= Configuring HAProxy PROXY protocol
11+
12+
[role="_abstract"]
13+
When Kroxylicious is deployed behind a load balancer that uses the https://www.haproxy.org/download/3.3/doc/proxy-protocol.txt[HAProxy PROXY protocol], you can configure PROXY protocol handling so that the original client connection metadata (source address, destination address, ports) is preserved.
14+
15+
Both PROXY protocol v1 (text) and v2 (binary, including TLV extensions) are supported.
16+
17+
== Proxy protocol modes
18+
19+
The `proxyProtocol.mode` property controls how Kroxylicious handles incoming connections:
20+
21+
`required`:: Every incoming connection *must* begin with a PROXY protocol header.
22+
Connections without a valid header are rejected immediately with a warning log.
23+
Use this when Kroxylicious is always behind a PROXY protocol-capable load balancer.
24+
25+
`allowed`:: Kroxylicious inspects the first bytes of each connection and automatically detects whether a PROXY protocol header is present.
26+
If detected, the header is decoded and the original client address is extracted.
27+
If not, the bytes are passed through to the Kafka protocol decoder.
28+
Use this when the same listener may receive both proxied and direct connections.
29+
30+
`disabled`:: No PROXY protocol handling (default).
31+
All bytes are treated as Kafka protocol data.
32+
33+
.Configuration fragment with PROXY protocol in required mode
34+
[source,yaml]
35+
----
36+
proxyProtocol:
37+
mode: required
38+
----
39+
40+
.Configuration fragment with PROXY protocol in allowed mode
41+
[source,yaml]
42+
----
43+
proxyProtocol:
44+
mode: allowed
45+
----
46+
47+
[IMPORTANT]
48+
====
49+
When `mode` is set to `required`, all incoming connections must include a PROXY protocol header.
50+
If a client connects directly without going through a PROXY protocol-capable load balancer, the connection will be rejected and Kroxylicious will log a warning:
51+
52+
`Connection rejected — expected PROXY protocol header but received non-PROXY data.
53+
Ensure the upstream load balancer is configured to send PROXY protocol headers,
54+
or set proxyProtocol mode to 'allowed' or 'disabled'.`
55+
====
56+
57+
[IMPORTANT]
58+
====
59+
When `mode` is set to `allowed`, any client that can connect directly to Kroxylicious (bypassing the load balancer) can send a PROXY protocol header with a spoofed source address.
60+
Only use `allowed` mode when your network topology guarantees that untrusted clients cannot reach the proxy port directly, or when source address spoofing is an acceptable risk.
61+
If all connections come through a PROXY protocol-capable load balancer, prefer `required` mode instead.
62+
====

kroxylicious-integration-test-support/src/test/java/io/kroxylicious/proxy/config/ConfigurationTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ void shouldRejectFilterDefinitionsWithSameName() {
584584
null,
585585
false,
586586
development,
587+
null,
587588
null))
588589
.isInstanceOf(IllegalConfigurationException.class)
589590
.hasMessage("'filterDefinitions' contains multiple items with the same names: [foo]");
@@ -601,6 +602,7 @@ void shouldRejectMissingDefaultFilter() {
601602
null,
602603
false,
603604
development,
605+
null,
604606
null))
605607
.isInstanceOf(IllegalConfigurationException.class)
606608
.hasMessage("'defaultFilters' references filters not defined in 'filterDefinitions': [missing]");
@@ -621,6 +623,7 @@ void shouldRejectMissingClusterFilter() {
621623
virtualClusters,
622624
null, false,
623625
development,
626+
null,
624627
null))
625628
.isInstanceOf(IllegalConfigurationException.class)
626629
.hasMessage("'virtualClusters.vc1.filters' references filters not defined in 'filterDefinitions': [missing]");
@@ -647,6 +650,7 @@ void shouldRejectUnusedFilterDefinition() {
647650
null,
648651
false,
649652
development,
653+
null,
650654
null))
651655
.isInstanceOf(IllegalConfigurationException.class)
652656
.hasMessage("'filterDefinitions' defines filters which are not used in 'defaultFilters' or in any virtual cluster's 'filters': [unused]");
@@ -670,6 +674,7 @@ void virtualClusterModelShouldUseCorrectFilters() {
670674
null,
671675
false,
672676
Optional.empty(),
677+
null,
673678
null);
674679

675680
// When
@@ -684,6 +689,42 @@ void virtualClusterModelShouldUseCorrectFilters() {
684689
.hasValueSatisfying(vcm -> assertThat(vcm.getFilters()).singleElement().extracting(NamedFilterDefinition::type).isEqualTo("Bar"));
685690
}
686691

692+
@Test
693+
void proxyProtocolModeShouldReturnRequiredWhenRequired() {
694+
Configuration configuration = new Configuration(null, null, null,
695+
List.of(buildVirtualCluster("vc", "x:9092", null)),
696+
null, false, Optional.empty(), null,
697+
new ProxyProtocolConfig(ProxyProtocolMode.REQUIRED));
698+
assertThat(configuration.proxyProtocolMode()).isEqualTo(ProxyProtocolMode.REQUIRED);
699+
}
700+
701+
@Test
702+
void proxyProtocolModeShouldReturnAllowedWhenAllowed() {
703+
Configuration configuration = new Configuration(null, null, null,
704+
List.of(buildVirtualCluster("vc", "x:9092", null)),
705+
null, false, Optional.empty(), null,
706+
new ProxyProtocolConfig(ProxyProtocolMode.ALLOWED));
707+
assertThat(configuration.proxyProtocolMode()).isEqualTo(ProxyProtocolMode.ALLOWED);
708+
}
709+
710+
@Test
711+
void proxyProtocolModeShouldReturnDisabledWhenDisabled() {
712+
Configuration configuration = new Configuration(null, null, null,
713+
List.of(buildVirtualCluster("vc", "x:9092", null)),
714+
null, false, Optional.empty(), null,
715+
new ProxyProtocolConfig(ProxyProtocolMode.DISABLED));
716+
assertThat(configuration.proxyProtocolMode()).isEqualTo(ProxyProtocolMode.DISABLED);
717+
}
718+
719+
@Test
720+
void proxyProtocolDefaultsToDisabledWhenNull() {
721+
Configuration configuration = new Configuration(null, null, null,
722+
List.of(buildVirtualCluster("vc", "x:9092", null)),
723+
null, false, Optional.empty(), null,
724+
null);
725+
assertThat(configuration.proxyProtocolMode()).isEqualTo(ProxyProtocolMode.DISABLED);
726+
}
727+
687728
@NonNull
688729
private static VirtualCluster buildVirtualCluster(String virtualClusterName, String targetBootstrap, @Nullable List<String> filterNames) {
689730
return new VirtualCluster(virtualClusterName, new TargetCluster(targetBootstrap, Optional.empty()),

kroxylicious-integration-tests/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@
222222
<artifactId>netty-buffer</artifactId>
223223
<scope>test</scope>
224224
</dependency>
225+
<dependency>
226+
<groupId>io.netty</groupId>
227+
<artifactId>netty-codec-haproxy</artifactId>
228+
<scope>test</scope>
229+
</dependency>
230+
<dependency>
231+
<groupId>io.netty</groupId>
232+
<artifactId>netty-handler</artifactId>
233+
<scope>test</scope>
234+
</dependency>
225235
<dependency>
226236
<groupId>org.apache.kafka</groupId>
227237
<artifactId>kafka-clients</artifactId>

0 commit comments

Comments
 (0)