Skip to content

Commit

Permalink
4.x: Initial support for proxy protocol V1 and V2 (#7829)
Browse files Browse the repository at this point in the history
* Initial support for proxy protocol data V1 and V2. Minor changes to API to make protocol data available to application handlers.

* Uses ListenerConfig for proxy protocol. Return Optional instead of null.

* Initial support for proxy protocol version 2.

* Support for IPv4 in V2.

* Additional unit testing and a new integration test for V1 and V2 of the proxy protocol.

* Make X-Forwarded-For and X-Forwarded-Port available as request headers when using proxy protocol.

* Use explicit instead of default encoding.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Switch to using HexFormat. Improved new Javadoc. Cleanup constructor params.

---------

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
  • Loading branch information
spericas authored Nov 20, 2023
1 parent 6835b45 commit dc4db18
Show file tree
Hide file tree
Showing 13 changed files with 840 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,22 @@ public void request(String method, String path, String protocol, String host, It
}
}

/**
* Write raw proxy protocol header before a request.
*
* @param header header to write
*/
public void writeProxyHeader(byte[] header) {
try {
if (socket == null) {
connect();
}
socket.getOutputStream().write(header);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

/**
* Disconnect from server socket.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
import io.helidon.webserver.http2.spi.Http2SubProtocolSelector;
import io.helidon.webserver.spi.ServerConnection;

import static io.helidon.http.HeaderNames.X_FORWARDED_FOR;
import static io.helidon.http.HeaderNames.X_FORWARDED_PORT;
import static io.helidon.http.HeaderNames.X_HELIDON_CN;
import static io.helidon.http.http2.Http2Util.PREFACE_LENGTH;
import static java.lang.System.Logger.Level.DEBUG;
Expand Down Expand Up @@ -614,6 +616,19 @@ private void doHeaders(Semaphore requestSemaphore) {
ctx.remotePeer().tlsCertificates()
.flatMap(TlsUtils::parseCn)
.ifPresent(cn -> connectionHeaders.add(X_HELIDON_CN, cn));

// proxy protocol related headers X-Forwarded-For and X-Forwarded-Port
ctx.proxyProtocolData().ifPresent(proxyProtocolData -> {
String sourceAddress = proxyProtocolData.sourceAddress();
if (!sourceAddress.isEmpty()) {
connectionHeaders.add(X_FORWARDED_FOR, sourceAddress);
}
int sourcePort = proxyProtocolData.sourcePort();
if (sourcePort != -1) {
connectionHeaders.set(X_FORWARDED_PORT, sourcePort);
}
});

initConnectionHeaders = false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.InputStream;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

Expand All @@ -38,6 +39,7 @@
import io.helidon.http.media.ReadableEntity;
import io.helidon.webserver.ConnectionContext;
import io.helidon.webserver.ListenerContext;
import io.helidon.webserver.ProxyProtocolData;
import io.helidon.webserver.http.HttpSecurity;
import io.helidon.webserver.http.RoutingRequest;

Expand Down Expand Up @@ -220,6 +222,11 @@ public void streamFilter(UnaryOperator<InputStream> filterFunction) {
this.streamFilter = it -> filterFunction.apply(current.apply(it));
}

@Override
public Optional<ProxyProtocolData> proxyProtocolData() {
return ctx.proxyProtocolData();
}

private UriInfo createUriInfo() {
return ctx.listenerContext().config().requestedUriDiscoveryContext().uriInfo(remotePeer().address().toString(),
localPeer().address().toString(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.webserver.tests;

import java.util.HexFormat;

import io.helidon.common.testing.http.junit5.SocketHttpClient;
import io.helidon.http.HeaderNames;
import io.helidon.http.Method;
import io.helidon.http.Status;
import io.helidon.webserver.ProxyProtocolData;
import io.helidon.webserver.WebServerConfig;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.testing.junit5.ServerTest;
import io.helidon.webserver.testing.junit5.SetUpRoute;
import io.helidon.webserver.testing.junit5.SetUpServer;
import org.junit.jupiter.api.Test;

import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;

@ServerTest
class ProxyProtocolTest {

static final String V2_PREFIX = "0D:0A:0D:0A:00:0D:0A:51:55:49:54:0A";

private final static HexFormat hexFormat = HexFormat.of().withUpperCase().withDelimiter(":");

private final SocketHttpClient socketHttpClient;

ProxyProtocolTest(SocketHttpClient socketHttpClient) {
this.socketHttpClient = socketHttpClient;
}

@SetUpServer
static void setupServer(WebServerConfig.Builder builder) {
builder.enableProxyProtocol(true);
}

@SetUpRoute
static void routing(HttpRules routing) {
routing.get("/", (req, res) -> {
ProxyProtocolData data = req.proxyProtocolData().orElse(null);
if (data != null
&& data.family() == ProxyProtocolData.Family.IPv4
&& data.protocol() == ProxyProtocolData.Protocol.TCP
&& data.sourceAddress().equals("192.168.0.1")
&& data.destAddress().equals("192.168.0.11")
&& data.sourcePort() == 56324
&& data.destPort() == 443
&& "192.168.0.1".equals(req.headers().first(HeaderNames.X_FORWARDED_FOR).orElse(null))
&& "56324".equals(req.headers().first(HeaderNames.X_FORWARDED_PORT).orElse(null))) {
res.status(Status.OK_200).send();
return;
}
res.status(Status.INTERNAL_SERVER_ERROR_500).send();
});
}

/**
* V1 encoding in this test was manually verified with Wireshark.
*/
@Test
void testProxyProtocolV1IPv4() {
socketHttpClient.writeProxyHeader("PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n".getBytes(US_ASCII));
String s = socketHttpClient.sendAndReceive(Method.GET, "");
assertThat(s, startsWith("HTTP/1.1 200 OK"));
}

/**
* V2 encoding in this test was manually verified with Wireshark.
*/
@Test
void testProxyProtocolV2IPv4() {
String header = V2_PREFIX
+ ":20:11:00:0C" // version, family/protocol, length
+ ":C0:A8:00:01" // 192.168.0.1
+ ":C0:A8:00:0B" // 192.168.0.11
+ ":DC:04" // 56324
+ ":01:BB"; // 443
socketHttpClient.writeProxyHeader(hexFormat.parseHex(header));
String s = socketHttpClient.sendAndReceive(Method.GET, "");
assertThat(s, startsWith("HTTP/1.1 200 OK"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.helidon.webserver;

import java.util.Optional;
import java.util.concurrent.ExecutorService;

import io.helidon.common.buffers.DataReader;
Expand Down Expand Up @@ -60,4 +61,14 @@ public interface ConnectionContext extends SocketContext {
* @return rouer
*/
Router router();

/**
* Proxy protocol header data.
*
* @return protocol header data if proxy protocol is enabled on socket
* @see ListenerConfig#enableProxyProtocol()
*/
default Optional<ProxyProtocolData> proxyProtocolData() {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;

Expand Down Expand Up @@ -64,11 +65,13 @@ class ConnectionHandler implements InterruptableTask<Void>, ConnectionContext {
private final String serverChannelId;
private final Router router;
private final Tls tls;
private final ListenerConfig listenerConfig;

private ServerConnection connection;
private HelidonSocket helidonSocket;
private DataReader reader;
private SocketWriter writer;
private ProxyProtocolData proxyProtocolData;

ConnectionHandler(ListenerContext listenerContext,
Semaphore connectionSemaphore,
Expand All @@ -89,6 +92,7 @@ class ConnectionHandler implements InterruptableTask<Void>, ConnectionContext {
this.serverChannelId = serverChannelId;
this.router = router;
this.tls = tls;
this.listenerConfig = listenerContext.config();
}

@Override
Expand All @@ -100,6 +104,12 @@ public boolean canInterrupt() {
public final void run() {
String channelId = "0x" + HexFormat.of().toHexDigits(System.identityHashCode(socket));

// proxy protocol before SSL handshake
if (listenerConfig.enableProxyProtocol()) {
ProxyProtocolHandler handler = new ProxyProtocolHandler(socket, channelId);
proxyProtocolData = handler.get();
}

// handle SSL and init helidonSocket, reader and writer
try {
if (tls.enabled()) {
Expand Down Expand Up @@ -226,6 +236,11 @@ public Router router() {
return router;
}

@Override
public Optional<ProxyProtocolData> proxyProtocolData() {
return Optional.ofNullable(proxyProtocolData);
}

private ServerConnection identifyConnection() {
try {
reader.ensureAvailable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ interface ListenerConfigBlueprint {
*/
Optional<Context> listenerContext();

/**
* Enable proxy protocol support for this socket. This protocol is supported by
* some load balancers/reverse proxies as a means to convey client information that
* would otherwise be lost. If enabled, the proxy protocol header must be present
* on every new connection established with your server. For more information,
* see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">
* the specification</a>. Default is {@code false}.
*
* @return proxy support status
*/
@Option.Default("false")
boolean enableProxyProtocol();

/**
* Requested URI discovery context.
*
Expand Down
Loading

0 comments on commit dc4db18

Please sign in to comment.