This is an automated email from the ASF dual-hosted git repository.
twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new 8925c4f7a Add support for Proxy Protocol V2.
new 340312c06 Merge pull request #404 from
fherbreteau/sshd-contrib_ProxyProtocol_V2
8925c4f7a is described below
commit 8925c4f7ac8652814592539499f4929654b3deb1
Author: f.herbreteau <[email protected]>
AuthorDate: Fri Aug 11 00:40:21 2023 +0200
Add support for Proxy Protocol V2.
HAProxy protocol V2 has several improvements over the V1 protocol. Add an
acceptor and parsing support for the protocol as described at [1].
Also add a Buffer.getUShort() method to read an unsigned 16bit value from
a Buffer. The proxy protocol V2 uses such values.
[1] https://www.haproxy.org/download/2.7/doc/proxy-protocol.txt
Signed-off-by: f.herbreteau <[email protected]>
---
docs/extensions.md | 4 +
.../org/apache/sshd/common/util/buffer/Buffer.java | 4 +
sshd-contrib/pom.xml | 10 +
.../proxyprotocolv2/ProxyProtocolV2Acceptor.java | 122 +++++++
.../session/proxyprotocolv2/data/AddressData.java | 139 ++++++++
.../proxyprotocolv2/data/FamilyAndTransport.java | 132 ++++++++
.../proxyprotocolv2/data/VersionAndCommand.java | 66 ++++
.../exception/ProxyProtocolException.java | 77 +++++
.../session/proxyprotocolv2/utils/ProxyUtils.java | 50 +++
.../ProxyProtocolV2AcceptorTest.java | 350 +++++++++++++++++++++
10 files changed, 954 insertions(+)
diff --git a/docs/extensions.md b/docs/extensions.md
index fa2c61a6e..35cc1d089 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -57,6 +57,10 @@ methods that provide SFTP file information - including
reading data - and those
* `ProxyProtocolAcceptor` - A working prototype to support the PROXY protocol
as described in
[HAProxy
Documentation](http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
+* `ProxyProtocolV2Acceptor` - A working prototype to support the PROXY
protocol V1 and V2 as described in
+[HAProxy
Documentation](http://www.haproxy.org/download/2.7/doc/proxy-protocol.txt).
This acceptor extends
+the `ProxyProtocolAcceptor` for V1 Protocol.
+
* `ThrottlingPacketWriter` - An example of a way to overcome big window sizes
when sending data - as
described in [SSHD-754](https://issues.apache.org/jira/browse/SSHD-754) and
[SSHD-768](https://issues.apache.org/jira/browse/SSHD-768)
diff --git
a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
index c9d73b269..5466aee49 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
@@ -292,6 +292,10 @@ public abstract class Buffer implements Readable {
return v;
}
+ public int getUShort() {
+ return getShort() & 0xFFFF;
+ }
+
public int getInt() {
return (int) getUInt();
}
diff --git a/sshd-contrib/pom.xml b/sshd-contrib/pom.xml
index 835ca88c1..d26e4e73b 100644
--- a/sshd-contrib/pom.xml
+++ b/sshd-contrib/pom.xml
@@ -80,6 +80,16 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>3.24.2</version>
+ </dependency>
</dependencies>
<build>
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2Acceptor.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2Acceptor.java
new file mode 100644
index 000000000..05d5e9330
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2Acceptor.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2;
+
+import java.util.Arrays;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import
org.apache.sshd.contrib.server.session.proxyprotocol.ProxyProtocolAcceptor;
+import org.apache.sshd.contrib.server.session.proxyprotocolv2.data.AddressData;
+import
org.apache.sshd.contrib.server.session.proxyprotocolv2.data.FamilyAndTransport;
+import
org.apache.sshd.contrib.server.session.proxyprotocolv2.data.VersionAndCommand;
+import org.apache.sshd.contrib.server.session.proxyprotocolv2.utils.ProxyUtils;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * A working prototype to support PROXY protocol v2 as described in
+ * <A
HREF="https://www.haproxy.org/download/2.7/doc/proxy-protocol.txt">HAProxy
Documentation</A>.
+ * <p>
+ * This <code>ServerProxyAcceptor</code> can process PROXY protocol v1 and v2.
+ * </p>
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public class ProxyProtocolV2Acceptor extends ProxyProtocolAcceptor {
+
+ private static final byte[] PROXY_V2_HEADER
+ = new byte[] { 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51,
0x55, 0x49, 0x54, 0x0A };
+
+ private static final char FIELD_SEPARATOR = ' ';
+
+ public ProxyProtocolV2Acceptor() {
+ super();
+ }
+
+ @Override
+ public boolean acceptServerProxyMetadata(ServerSession session, Buffer
buffer) throws Exception {
+ int mark = buffer.rpos();
+ int dataLen = buffer.available();
+ if (dataLen < PROXY_V2_HEADER.length) {
+ if (log.isDebugEnabled()) {
+ log.debug("acceptServerProxyMetadata(session={}) incomplete
data - {}/{}", session, dataLen,
+ PROXY_V2_HEADER.length);
+ }
+ return false;
+ }
+
+ byte[] proxyV2Header = new byte[PROXY_V2_HEADER.length];
+ buffer.getRawBytes(proxyV2Header);
+
+ if (!Arrays.equals(PROXY_V2_HEADER, proxyV2Header)) {
+ buffer.rpos(mark); // Rewind the buffer to allow further reading
+ return super.acceptServerProxyMetadata(session, buffer);
+ }
+ return readProxyV2Header(session, mark, buffer);
+ }
+
+ protected boolean readProxyV2Header(ServerSession session, int
markPosition, Buffer buffer) throws Exception {
+ if (log.isDebugEnabled()) {
+ int mark = buffer.rpos();
+ buffer.rpos(markPosition);
+ log.debug("readProxyV2Header(session={}) processing Proxy Protocol
V2 buffer : [{}]", session,
+ ProxyUtils.toHexString(buffer, mark));
+ }
+ StringBuilder proxyPayload = new StringBuilder();
+ // Read the version and command information
+ VersionAndCommand versionAndCommand =
VersionAndCommand.extractValue(log, session, buffer);
+ proxyPayload.append(versionAndCommand.name());
+ // Read the family and transport.
+ FamilyAndTransport familyAndTransport =
FamilyAndTransport.extractValue(log, session, buffer);
+ proxyPayload.append(FIELD_SEPARATOR).append(familyAndTransport.name());
+ // Read the data length
+ int dataLength = buffer.getUShort();
+ // Unix Socket are not supported by SSHD
+ if (familyAndTransport.hasSockAddress()) {
+ log.warn("parseProxyHeader(session={}) unsupported sub-protocol -
{} - continue as usual", session,
+ familyAndTransport);
+ // Skip socket address data
+ AddressData.skipUnprocessedData(log, session, buffer,
FamilyAndTransport.UNSPEC, dataLength);
+ return true;
+ }
+ // Read the address Data (Host and Port for source and dest)
+ AddressData data = AddressData.extractAddressData(log, session,
buffer, familyAndTransport, dataLength);
+ proxyPayload.append(FIELD_SEPARATOR).append(data);
+ // Parse the converted proxy header
+ return parseProxyHeader(session, proxyPayload.toString(),
markPosition, buffer);
+ }
+
+ @Override
+ protected boolean parseProxyHeader(ServerSession session, String
proxyHeader, int markPosition, Buffer buffer)
+ throws Exception {
+ String[] proxyFields = GenericUtils.split(proxyHeader,
FIELD_SEPARATOR);
+ // Trim all fields just in case more than one space used
+ for (int index = 0; index < proxyFields.length; index++) {
+ String f = proxyFields[index];
+ proxyFields[index] = GenericUtils.trimToEmpty(f);
+ }
+ // Nothing to do for local proxy protocol
+ if ("LOCAL".equals(proxyFields[0])) {
+ log.debug("parseProxyHeader(session={}) local proxy check",
session);
+ return true;
+ }
+ return super.parseProxyHeader(session, proxyHeader, markPosition,
buffer);
+ }
+}
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/AddressData.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/AddressData.java
new file mode 100644
index 000000000..db9b6651e
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/AddressData.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2.data;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.contrib.server.session.proxyprotocolv2.utils.ProxyUtils;
+import org.apache.sshd.server.session.ServerSession;
+import org.slf4j.Logger;
+
+
+/**
+ * Address data structure.
+ * <p>
+ * Starting from the 17th byte, addresses are presented in network byte order.
+ * </p>
+ * <p>
+ * The address order is always the same : - source layer 3 address in network
byte order - destination layer 3 address
+ * in network byte order - source layer 4 address if any, in network byte
order (port) - destination layer 4 address if
+ * any, in network byte order (port)
+ * </p>
+ * <p>
+ * The address block may directly be sent from or received into the following
union which makes it easy to cast from/to
+ * the relevant socket native structs depending on the address type :
+ * </p>
+ *
+ * <pre>
+ * union proxy_addr {
+ * struct { // for TCP/UDP over IPv4, len = 12
+ * uint32_t src_addr;
+ * uint32_t dst_addr;
+ * uint16_t src_port;
+ * uint16_t dst_port;
+ * }ipv4_addr;
+ * struct{ // for TCP/UDP over IPv6, len = 36
+ * uint8_t src_addr[16];
+ * uint8_t dst_addr[16];
+ * uint16_t src_port;
+ * uint16_t dst_port;
+ * }ipv6_addr;
+ * struct{ // for AF_UNIX sockets, len = 216
+ * uint8_t src_addr[108];
+ * uint8_t dst_addr[108];
+ * }unix_addr;
+ * };
+ * </pre>
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public final class AddressData {
+
+ private final String srcAddress;
+ private final String dstAddress;
+
+ private final int srcPort;
+ private final int dstPort;
+
+ private AddressData(String srcAddress, String dstAddress, int srcPort, int
dstPort) {
+ this.srcAddress = srcAddress;
+ this.dstAddress = dstAddress;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ }
+
+ public static AddressData extractAddressData(Logger logger,
+ ServerSession session,
+ Buffer buffer,
+ FamilyAndTransport familyAndTransport,
+ int dataLength)
+ throws IOException {
+ String srcAddress = extractAddresses(buffer, familyAndTransport);
+ String dstAddress = extractAddresses(buffer, familyAndTransport);
+ int srcPort = extractPort(buffer, familyAndTransport);
+ int dstPort = extractPort(buffer, familyAndTransport);
+ skipUnprocessedData(logger, session, buffer, familyAndTransport,
dataLength);
+ return new AddressData(srcAddress, dstAddress, srcPort, dstPort);
+ }
+
+ public static void skipUnprocessedData(
+ Logger logger,
+ ServerSession session,
+ Buffer buffer,
+ FamilyAndTransport familyAndTransport,
+ int dataLength) {
+ int remaining = dataLength - familyAndTransport.getDataLength();
+ if (remaining > 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("extractAddressData({}) skipping additional datas
[{}]",
+ session,
+ ProxyUtils.toHexString(buffer, buffer.rpos()));
+ }
+ // Insure the remaining bytes are available
+ buffer.ensureAvailable(remaining);
+ // Skip all extra datas
+ buffer.rpos(buffer.rpos() + remaining);
+ }
+ }
+
+ private static String extractAddresses(Buffer buffer, FamilyAndTransport
familyAndTransport)
+ throws IOException {
+ byte[] datas = new byte[familyAndTransport.getAddressLength()];
+ buffer.getRawBytes(datas);
+ if (familyAndTransport.hasInetAddress()) {
+ return InetAddress.getByAddress(datas).getHostAddress();
+ }
+ return "";
+ }
+
+ private static int extractPort(Buffer buffer, FamilyAndTransport
familyAndTransport) {
+ if (familyAndTransport.hasPort()) {
+ return buffer.getUShort();
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return String.join(" ", srcAddress, dstAddress,
Integer.toString(srcPort), Integer.toString(dstPort));
+ }
+}
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/FamilyAndTransport.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/FamilyAndTransport.java
new file mode 100644
index 000000000..dd36c88be
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/FamilyAndTransport.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2.data;
+
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+import
org.apache.sshd.contrib.server.session.proxyprotocolv2.exception.ProxyProtocolException;
+import org.apache.sshd.server.session.ServerSession;
+import org.slf4j.Logger;
+
+/**
+ * Family and Transport Enumeration.
+ * <p>
+ * The 14th byte contains the transport protocol and address family. The
highest 4 bits contain the address family, the
+ * lowest 4 bits contain the protocol.
+ * </p>
+ * <p>
+ * The address family maps to the original socket family without necessarily
matching the values internally used by the
+ * system. It may be one of : - 0x0 : AF_UNSPEC : the connection is forwarded
for an unknown, unspecified or unsupported
+ * protocol. The sender should use this family when sending LOCAL commands or
when dealing with unsupported protocol
+ * families. The receiver is free to accept the connection anyway and use the
real endpoint addresses or to reject it.
+ * The receiver should ignore address information. - 0x1 : AF_INET : the
forwarded connection uses the AF_INET address
+ * family (IPv4). The addresses are exactly 4 bytes each in network byte
order, followed by transport protocol
+ * information (typically ports). - 0x2 : AF_INET6 : the forwarded connection
uses the AF_INET6 address family (IPv6).
+ * The addresses are exactly 16 bytes each in network byte order, followed by
transport protocol information (typically
+ * ports). - 0x3 : AF_UNIX : the forwarded connection uses the AF_UNIX address
family (UNIX). The addresses are exactly
+ * 108 bytes each. - other values are unspecified and must not be emitted in
version 2 of this protocol and must be
+ * rejected as invalid by receivers.
+ * </p>
+ * <p>
+ * The transport protocol is specified in the lowest 4 bits of the 14th byte :
- 0x0 : UNSPEC : the connection is
+ * forwarded for an unknown, unspecified or unsupported protocol. The sender
should use this family when sending LOCAL
+ * commands or when dealing with unsupported protocol families. The receiver
is free to accept the connection anyway and
+ * use the real endpoint addresses or to reject it. The receiver should ignore
address information. - 0x1 : STREAM : the
+ * forwarded connection uses a SOCK_STREAM protocol (eg: TCP or UNIX_STREAM).
When used with AF_INET/AF_INET6 (TCP), the
+ * addresses are followed by the source and destination ports represented on 2
bytes each in network byte order. - 0x2 :
+ * DGRAM : the forwarded connection uses a SOCK_DGRAM protocol (eg: UDP or
UNIX_DGRAM). When used with AF_INET/AF_INET6
+ * (UDP), the addresses are followed by the source and destination ports
represented on 2 bytes each in network byte
+ * order. - other values are unspecified and must not be emitted in version 2
of this protocol and must be rejected as
+ * invalid by receivers.
+ * </p>
+ * <p>
+ * In practice, the following protocol bytes are expected : - \x00 : UNSPEC :
the connection is forwarded for an
+ * unknown, unspecified or unsupported protocol. The sender should use this
family when sending LOCAL commands or when
+ * dealing with unsupported protocol families. When used with a LOCAL command,
the receiver must accept the connection
+ * and ignore any address information. For other commands, the receiver is
free to accept the connection anyway and use
+ * the real endpoints addresses or to reject the connection. The receiver
should ignore address information. - \x11 :
+ * TCP over IPv4 : the forwarded connection uses TCP over the AF_INET protocol
family. Address length is 2*4 + 2*2 = 12
+ * bytes. - \x12 : UDP over IPv4 : the forwarded connection uses UDP over the
AF_INET protocol family. Address length is
+ * 2*4 + 2*2 = 12 bytes. - \x21 : TCP over IPv6 : the forwarded connection
uses TCP over the AF_INET6 protocol family.
+ * Address length is 2*16 + 2*2 = 36 bytes. - \x22 : UDP over IPv6 : the
forwarded connection uses UDP over the AF_INET6
+ * protocol family. Address length is 2*16 + 2*2 = 36 bytes. - \x31 : UNIX
stream : the forwarded connection uses
+ * SOCK_STREAM over the AF_UNIX protocol family. Address length is 2*108 = 216
bytes. - \x32 : UNIX datagram : the
+ * forwarded connection uses SOCK_DGRAM over the AF_UNIX protocol family.
Address length is 2*108 = 216 bytes.
+ * </p>
+ * <p>
+ * Only the UNSPEC protocol byte (\x00) is mandatory to implement on the
receiver. A receiver is not required to
+ * implement other ones, provided that it automatically falls back to the
UNSPEC mode for the valid combinations above
+ * that it does not support.
+ * </p>
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public enum FamilyAndTransport {
+
+ UNSPEC((byte) 0x00, 0, 0),
+ TCP4((byte) 0x11, 4, 2),
+ UDP4((byte) 0x12, 4, 2),
+ TCP6((byte) 0x21, 16, 2),
+ UDP6((byte) 0x22, 16, 2),
+ SOCK_STREAM((byte) 0x31, 108, 0),
+ SOCK_DGRAM((byte) 0x32, 108, 0);
+
+ private final byte value;
+
+ private final int addressLength;
+
+ private final int portLength;
+
+ FamilyAndTransport(byte value, int addressLength, int portLength) {
+ this.value = value;
+ this.addressLength = addressLength;
+ this.portLength = portLength;
+ }
+
+ public static FamilyAndTransport extractValue(Logger logger, ServerSession
session, Buffer buffer)
+ throws ProxyProtocolException {
+ byte value = buffer.getByte();
+ return Stream.of(values())
+ .filter(val -> val.value == value)
+ .findFirst()
+ .orElseThrow(() ->
ProxyProtocolException.buildFamilyAndTransport(logger, session, value));
+ }
+
+ public int getAddressLength() {
+ return addressLength;
+ }
+
+ public int getDataLength() {
+ return addressLength * 2 + portLength * 2;
+ }
+
+ public boolean hasInetAddress() {
+ return addressLength > 0 && portLength > 0;
+ }
+
+ public boolean hasPort() {
+ return portLength > 0;
+ }
+
+ public boolean hasSockAddress() {
+ return addressLength > 0 && portLength == 0;
+ }
+}
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/VersionAndCommand.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/VersionAndCommand.java
new file mode 100644
index 000000000..79c8ac615
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/data/VersionAndCommand.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2.data;
+
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+import
org.apache.sshd.contrib.server.session.proxyprotocolv2.exception.ProxyProtocolException;
+import org.apache.sshd.server.session.ServerSession;
+import org.slf4j.Logger;
+
+/**
+ * Version and command enumeration.
+ * <p>
+ * The 13th byte is the protocol version and command. The highest four bits
contains the version. As of this
+ * specification, it must always be sent as \x2 and the receiver must only
accept this value.
+ * </p>
+ * <p>
+ * The lowest four bits represents the command : - \x0 : LOCAL : the
connection was established on purpose by the proxy
+ * without being relayed. The connection endpoints are the sender and the
receiver. Such connections exist when the
+ * proxy sends health-checks to the server. The receiver must accept this
connection as valid and must use the real
+ * connection endpoints and discard the protocol block including the family
which is ignored. - \x1 : PROXY : the
+ * connection was established on behalf of another node, and reflects the
original connection endpoints. The receiver
+ * must then use the information provided in the protocol block to get
original the address. - other values are
+ * unassigned and must not be emitted by senders. Receivers must drop
connections presenting unexpected values here.
+ * </p>
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public enum VersionAndCommand {
+
+ LOCAL((byte) 0x20),
+ PROXY((byte) 0x21);
+
+ private final byte value;
+
+ VersionAndCommand(byte value) {
+ this.value = value;
+ }
+
+ public static VersionAndCommand extractValue(Logger logger, ServerSession
session, Buffer buffer)
+ throws ProxyProtocolException {
+ byte value = buffer.getByte();
+ return Stream.of(values())
+ .filter(val -> val.value == value)
+ .findFirst()
+ .orElseThrow(() ->
ProxyProtocolException.buildVersionOrCommand(logger, session, value));
+ }
+}
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/exception/ProxyProtocolException.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/exception/ProxyProtocolException.java
new file mode 100644
index 000000000..2cb64ed57
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/exception/ProxyProtocolException.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2.exception;
+
+import org.apache.sshd.server.session.ServerSession;
+import org.slf4j.Logger;
+
+/**
+ * Blocking Exception that must block the connection.
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public final class ProxyProtocolException extends Exception {
+
+ public static final int PROXY_PROTOCOL_VERSION_2 = 2;
+
+ private static final int MAX_FAMILY_CODE = 3;
+
+ private static final long serialVersionUID = -7349477687125144605L;
+
+ private ProxyProtocolException(String message) {
+ super(message);
+ }
+
+ public static ProxyProtocolException buildVersionOrCommand(Logger log,
ServerSession session, byte value) {
+ byte valueLow = (byte) (value & 0x0F);
+ byte valueHeight = (byte) (value >> 4);
+ if (valueHeight != PROXY_PROTOCOL_VERSION_2) {
+ if (log.isDebugEnabled()) {
+ log.debug("readProxyV2Header(session={}) mismatched version in
proxy header: expected={}, actual={}",
+ session,
+ Integer.toHexString(PROXY_PROTOCOL_VERSION_2),
+ Integer.toHexString(valueHeight));
+ }
+ return new ProxyProtocolException("Invalid version " +
valueHeight);
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("readProxyV2Header(session={}) unassigned command in
proxy header: actual={}",
+ session, Integer.toHexString(valueLow));
+ }
+ return new ProxyProtocolException("Unassigned command " + valueLow);
+ }
+
+ public static ProxyProtocolException buildFamilyAndTransport(Logger log,
ServerSession session, byte value) {
+ byte valueLow = (byte) (value & 0x0F);
+ byte valueHeight = (byte) (value >> 4);
+ if (valueHeight > MAX_FAMILY_CODE) {
+ if (log.isDebugEnabled()) {
+ log.debug("readProxyV2Header(session={}) unspecified family in
proxy header: actual={}",
+ session, Integer.toHexString(valueHeight));
+ }
+ return new ProxyProtocolException("Unspecified family " +
valueHeight);
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("readProxyV2Header(session={}) unspecified transport in
proxy header: actual={}",
+ session, Integer.toHexString(valueLow));
+ }
+ return new ProxyProtocolException("Unspecified transport " + valueLow);
+ }
+}
diff --git
a/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/utils/ProxyUtils.java
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/utils/ProxyUtils.java
new file mode 100644
index 000000000..7cb0a1c49
--- /dev/null
+++
b/sshd-contrib/src/main/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/utils/ProxyUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2.utils;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * Proxy Utilities class
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+public final class ProxyUtils {
+
+ private ProxyUtils() {
+ // Utility Class
+ }
+
+ /**
+ * Create an hexadecimal string representation of the remaining content of
a buffer and reset the buffer after
+ * reading.
+ *
+ * @param buffer a buffer to read from
+ * @param markPosition the position from which to start.
+ * @return a hexadecimal string representation.
+ */
+ public static String toHexString(Buffer buffer, int markPosition) {
+ byte[] datas = new byte[buffer.available()];
+ buffer.getRawBytes(datas);
+ buffer.rpos(markPosition);
+ return BufferUtils.toHex(datas, 0, datas.length, ',');
+ }
+}
diff --git
a/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2AcceptorTest.java
b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2AcceptorTest.java
new file mode 100644
index 000000000..19329e46a
--- /dev/null
+++
b/sshd-contrib/src/test/java/org/apache/sshd/contrib/server/session/proxyprotocolv2/ProxyProtocolV2AcceptorTest.java
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.contrib.server.session.proxyprotocolv2;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import
org.apache.sshd.contrib.server.session.proxyprotocol.ProxyProtocolAcceptor;
+import
org.apache.sshd.contrib.server.session.proxyprotocolv2.exception.ProxyProtocolException;
+import org.apache.sshd.server.session.AbstractServerSession;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.type;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+
+/**
+ * Test Suite for Proxy Protocol V2 handling.
+ *
+ * @author Oodrive - François HERBRETEAU ([email protected])
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ProxyProtocolV2AcceptorTest {
+
+ private final ProxyProtocolAcceptor acceptor = new
ProxyProtocolV2Acceptor();
+
+ @Mock
+ private AbstractServerSession session;
+
+ @Captor
+ private ArgumentCaptor<InetSocketAddress> socketAddressArgumentCaptor;
+
+ public ProxyProtocolV2AcceptorTest() {
+ // Nothing to do.
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV1Tcp4() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer("PROXY TCP4 172.19.0.1
172.19.0.3 42272 80\r\n".getBytes());
+
+ // When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ // Then
+
verify(session).setClientAddress(socketAddressArgumentCaptor.capture());
+ assertThat(socketAddressArgumentCaptor.getValue())
+ .isNotNull()
+ .isInstanceOf(InetSocketAddress.class)
+ .asInstanceOf(type(InetSocketAddress.class))
+ .extracting(InetSocketAddress::getAddress)
+ .asInstanceOf(type(InetAddress.class))
+ .extracting(InetAddress::getHostAddress)
+ .asString()
+ .isEqualTo("172.19.0.1");
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(43);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV1Tpc6() throws Exception {
+ // Given
+ ByteArrayBuffer buffer
+ = new ByteArrayBuffer("PROXY TCP6 fe80::a00:27ff:fe9f:4016
fe80::a089:a3ff:fe15:e992 42272 80\r\n".getBytes());
+
+ // When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ // Then
+
verify(session).setClientAddress(socketAddressArgumentCaptor.capture());
+ assertThat(socketAddressArgumentCaptor.getValue())
+ .isNotNull()
+ .isInstanceOf(InetSocketAddress.class)
+ .asInstanceOf(type(InetSocketAddress.class))
+ .extracting(InetSocketAddress::getAddress)
+ .asInstanceOf(type(InetAddress.class))
+ .extracting(InetAddress::getHostAddress)
+ .asString()
+ .isEqualTo("fe80:0:0:0:a00:27ff:fe9f:4016");
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(72);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2Tcp4() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x11, 0x00, 0x0c, (byte) 0xac, 0x13, 0x00, 0x01, (byte)
0xac, 0x13, 0x00, 0x03,
+ (byte) 0xa5, 0x20, 0x00, (byte) 0x50 });
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+
verify(session).setClientAddress(socketAddressArgumentCaptor.capture());
+ assertThat(socketAddressArgumentCaptor.getValue())
+ .isNotNull()
+ .isInstanceOf(InetSocketAddress.class)
+ .asInstanceOf(type(InetSocketAddress.class))
+ .extracting(InetSocketAddress::getAddress)
+ .asInstanceOf(type(InetAddress.class))
+ .extracting(InetAddress::getHostAddress)
+ .asString()
+ .isEqualTo("172.19.0.1");
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(28);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2Tcp6() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x21, 0x00, 0x24, (byte) 0xfe, (byte) 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0a,
+ 0x00, 0x27, (byte) 0xff, (byte) 0xfe, (byte) 0x9f, 0x40, 0x16,
(byte) 0xfe, (byte) 0x80, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xa0, (byte) 0x89, (byte)
0xa3, (byte) 0xff, (byte) 0xfe,
+ 0x15, (byte) 0xe9, (byte) 0x92, (byte) 0xa5, 0x20, 0x00,
(byte) 0x50 });
+
+ // When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ // Then
+
verify(session).setClientAddress(socketAddressArgumentCaptor.capture());
+ assertThat(socketAddressArgumentCaptor.getValue())
+ .isNotNull()
+ .isInstanceOf(InetSocketAddress.class)
+ .asInstanceOf(type(InetSocketAddress.class))
+ .extracting(InetSocketAddress::getAddress)
+ .asInstanceOf(type(InetAddress.class))
+ .extracting(InetAddress::getHostAddress)
+ .asString()
+ .isEqualTo("fe80:0:0:0:a00:27ff:fe9f:4016");
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(52);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2UnixSocket() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x31, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00 });
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(20);
+
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2Udp4() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x12, 0x00, 0x0c, (byte) 0xac, 0x13, 0x00, 0x01, (byte)
0xac, 0x13, 0x00, 0x03,
+ (byte) 0xa5, 0x20, 0x00, (byte) 0x50 });
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(28);
+
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2Udp6() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x22, 0x00, 0x24, (byte) 0xfe, (byte) 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0a,
+ 0x00, 0x27, (byte) 0xff, (byte) 0xfe, (byte) 0x9f, 0x40, 0x16,
(byte) 0xfe, (byte) 0x80, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xa0, (byte) 0x89, (byte)
0xa3, (byte) 0xff, (byte) 0xfe,
+ 0x15, (byte) 0xe9, (byte) 0x92, (byte) 0xa5, 0x20, 0x00,
(byte) 0x50 });
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(52);
+
+ }
+
+ @Test
+ public void testHandlingOtherProtocolHeader() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new
ByteArrayBuffer("SSH-2.0-OpenSSH_9.3".getBytes());
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isEqualTo(19);
+ assertThat(buffer.rpos()).isZero();
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithLocalCommand() throws Exception
{
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x20, 0x00, 0x00, 0x00 });
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(16);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithExtendedData() throws Exception
{
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x20, 0x00, 0x00, 0x07, 0x03, 0x00, 0x04, (byte) 0xa9, (byte)
0xb8, 0x7e, (byte) 0x8f });
+
+ //When
+ assertTrue(acceptor.acceptServerProxyMetadata(session, buffer));
+ //Then
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isZero();
+ assertThat(buffer.rpos()).isEqualTo(23);
+
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithInvalidVersion() {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x31, 0x11, 0x00, 0x0c, (byte) 0xac, 0x13, 0x00, 0x01, (byte)
0xac, 0x13, 0x00, 0x03,
+ (byte) 0xa5, 0x20, 0x00, (byte) 0x50 });
+
+ //When
+ ProxyProtocolException exception
+ = assertThrows(ProxyProtocolException.class, () ->
acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(exception).hasMessage("Invalid version 3");
+ assertThat(buffer.available()).isEqualTo(15);
+ assertThat(buffer.rpos()).isEqualTo(13);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithUnassignedCommand() {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x23, 0x00, 0x00, 0x00 });
+
+ //When
+ ProxyProtocolException exception
+ = assertThrows(ProxyProtocolException.class, () ->
acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(exception).hasMessage("Unassigned command 3");
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isEqualTo(3);
+ assertThat(buffer.rpos()).isEqualTo(13);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithUnexpectedFamily() {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x40, 0x00, 0x00 });
+
+ //When
+ ProxyProtocolException exception
+ = assertThrows(ProxyProtocolException.class, () ->
acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(exception).hasMessage("Unspecified family 4");
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isEqualTo(2);
+ assertThat(buffer.rpos()).isEqualTo(14);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithUnexpectedTransport() {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] {
+ 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a,
+ 0x21, 0x14, 0x00, 0x00 });
+
+ //When
+ ProxyProtocolException exception
+ = assertThrows(ProxyProtocolException.class, () ->
acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ assertThat(exception).hasMessage("Unspecified transport 4");
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isEqualTo(2);
+ assertThat(buffer.rpos()).isEqualTo(14);
+ }
+
+ @Test
+ public void testHandlingProxyProtocolV2WithInvalidSize() throws Exception {
+ // Given
+ ByteArrayBuffer buffer = new ByteArrayBuffer(new byte[] { 0x00, 0x00,
0x00, 0x00 });
+
+ //When
+ assertFalse(acceptor.acceptServerProxyMetadata(session, buffer));
+
+ //Then
+ verify(session, never()).setClientAddress(any());
+ assertThat(buffer.available()).isEqualTo(4);
+ assertThat(buffer.rpos()).isZero();
+
+ }
+}