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 <f.herbret...@oodrive.com> 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 <f.herbret...@oodrive.com> --- 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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +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 (f.herbret...@oodrive.com) + */ +@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(); + + } +}