Added PROXY Protocol TLV support

Motivation:

The current PROXY protocol implementation does not have support for optional Type-Length-Value fields. This pull requests adds the TLV values as specified in the PROXY protocol specification (http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) and adds support for arbitrary TLVs.

Modifications:

The existing HAProxyMessage implements an additional TLV reading operation. A small bug in the AF_UNIX reader which didn’t set the reader index correctly was also fixed.

Result:

The PROXY protocol supports TLVs
This commit is contained in:
Georg Held 2016-09-28 23:04:07 +02:00 committed by Scott Mitchell
parent b03b0f22d1
commit 1f0d47dee7
5 changed files with 594 additions and 132 deletions

View File

@ -21,6 +21,10 @@ import io.netty.util.ByteProcessor;
import io.netty.util.CharsetUtil;
import io.netty.util.NetUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Message container for decoded HAProxy proxy protocol parameters
*/
@ -54,6 +58,7 @@ public final class HAProxyMessage {
private final String destinationAddress;
private final int sourcePort;
private final int destinationPort;
private final List<HAProxyTLV> tlvs;
/**
* Creates a new instance
@ -73,6 +78,18 @@ public final class HAProxyMessage {
HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
String sourceAddress, String destinationAddress, int sourcePort, int destinationPort) {
this(protocolVersion, command, proxiedProtocol,
sourceAddress, destinationAddress, sourcePort, destinationPort, Collections.<HAProxyTLV>emptyList());
}
/**
* Creates a new instance
*/
private HAProxyMessage(
HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
String sourceAddress, String destinationAddress, int sourcePort, int destinationPort,
List<HAProxyTLV> tlvs) {
if (proxiedProtocol == null) {
throw new NullPointerException("proxiedProtocol");
}
@ -90,6 +107,7 @@ public final class HAProxyMessage {
this.destinationAddress = destinationAddress;
this.sourcePort = sourcePort;
this.destinationPort = destinationPort;
this.tlvs = Collections.unmodifiableList(tlvs);
}
/**
@ -214,7 +232,71 @@ public final class HAProxyMessage {
dstPort = header.readUnsignedShort();
}
return new HAProxyMessage(ver, cmd, protAndFam, srcAddress, dstAddress, srcPort, dstPort);
final List<HAProxyTLV> tlvs = readTlvs(header);
return new HAProxyMessage(ver, cmd, protAndFam, srcAddress, dstAddress, srcPort, dstPort, tlvs);
}
private static List<HAProxyTLV> readTlvs(final ByteBuf header) {
HAProxyTLV haProxyTLV = readNextTLV(header);
if (haProxyTLV == null) {
return Collections.emptyList();
}
// In most cases there are less than 4 TLVs available
List<HAProxyTLV> haProxyTLVs = new ArrayList<HAProxyTLV>(4);
do {
haProxyTLVs.add(haProxyTLV);
if (haProxyTLV instanceof HAProxySSLTLV) {
haProxyTLVs.addAll(((HAProxySSLTLV) haProxyTLV).encapsulatedTLVs());
}
} while ((haProxyTLV = readNextTLV(header)) != null);
return haProxyTLVs;
}
private static HAProxyTLV readNextTLV(final ByteBuf header) {
// We need at least 4 bytes for a TLV
if (header.readableBytes() < 4) {
return null;
}
final byte typeAsByte = header.readByte();
final HAProxyTLV.Type type = HAProxyTLV.Type.typeForByteValue(typeAsByte);
final int length = header.readUnsignedShort();
switch (type) {
case PP2_TYPE_SSL:
final ByteBuf rawContent = header.retainedSlice(header.readerIndex(), length);
final ByteBuf byteBuf = header.readSlice(length);
final byte client = byteBuf.readByte();
final int verify = byteBuf.readInt();
if (byteBuf.readableBytes() >= 4) {
final List<HAProxyTLV> encapsulatedTlvs = new ArrayList<HAProxyTLV>(4);
do {
final HAProxyTLV haProxyTLV = readNextTLV(byteBuf);
if (haProxyTLV == null) {
break;
}
encapsulatedTlvs.add(haProxyTLV);
} while (byteBuf.readableBytes() >= 4);
return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent);
}
return new HAProxySSLTLV(verify, client, Collections.<HAProxyTLV>emptyList(), rawContent);
// If we're not dealing with a SSL Type, we can use the same mechanism
case PP2_TYPE_ALPN:
case PP2_TYPE_AUTHORITY:
case PP2_TYPE_SSL_VERSION:
case PP2_TYPE_SSL_CN:
case PP2_TYPE_NETNS:
case OTHER:
return new HAProxyTLV(type, typeAsByte, header.readRetainedSlice(length));
default:
return null;
}
}
/**
@ -428,4 +510,13 @@ public final class HAProxyMessage {
public int destinationPort() {
return destinationPort;
}
/**
* Returns a list of {@link HAProxyTLV} or an empty list if no TLVs are present.
* <p>
* TLVs are only available for the Proxy Protocol V2
*/
public List<HAProxyTLV> tlvs() {
return tlvs;
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2016 The Netty Project
*
* The Netty Project 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 io.netty.handler.codec.haproxy;
import io.netty.buffer.ByteBuf;
import java.util.Collections;
import java.util.List;
/**
* Represents a {@link HAProxyTLV} of the type {@link HAProxyTLV.Type#PP2_TYPE_SSL}.
* This TLV encapsulates other TLVs and has additional information like verification information and a client bitfield.
*/
public final class HAProxySSLTLV extends HAProxyTLV {
private final int verify;
private final List<HAProxyTLV> tlvs;
private final byte clientBitField;
/**
* Creates a new HAProxySSLTLV
*
* @param verify the verification result as defined in the specification for the pp2_tlv_ssl struct (see
* http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt)
* @param clientBitField the bitfield with client information
* @param tlvs the encapsulated {@link HAProxyTLV}s
* @param rawContent the raw TLV content
*/
HAProxySSLTLV(final int verify, final byte clientBitField, final List<HAProxyTLV> tlvs, final ByteBuf rawContent) {
super(Type.PP2_TYPE_SSL, (byte) 0x20, rawContent);
this.verify = verify;
this.tlvs = Collections.unmodifiableList(tlvs);
this.clientBitField = clientBitField;
}
/**
* Returns {@code true} if the bit field for PP2_CLIENT_CERT_CONN was set
*/
public boolean isPP2ClientCertConn() {
return (clientBitField & 0x2) != 0;
}
/**
* Returns {@code true} if the bit field for PP2_CLIENT_SSL was set
*/
public boolean isPP2ClientSSL() {
return (clientBitField & 0x1) != 0;
}
/**
* Returns {@code true} if the bit field for PP2_CLIENT_CERT_SESS was set
*/
public boolean isPP2ClientCertSess() {
return (clientBitField & 0x4) != 0;
}
/**
* Returns the verification result
*/
public int verify() {
return verify;
}
/**
* Returns an unmodifiable Set of encapsulated {@link HAProxyTLV}s.
*/
public List<HAProxyTLV> encapsulatedTLVs() {
return tlvs;
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright 2016 The Netty Project
*
* The Netty Project 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 io.netty.handler.codec.haproxy;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.DefaultByteBufHolder;
import static io.netty.util.internal.ObjectUtil.*;
/**
* A Type-Length Value (TLV vector) that can be added to the PROXY protocol
* to include additional information like SSL information.
*
* @see HAProxySSLTLV
*/
public class HAProxyTLV extends DefaultByteBufHolder {
private final Type type;
private final byte typeByteValue;
/**
* The registered types a TLV can have regarding the PROXY protocol 1.5 spec
*/
public enum Type {
PP2_TYPE_ALPN,
PP2_TYPE_AUTHORITY,
PP2_TYPE_SSL,
PP2_TYPE_SSL_VERSION,
PP2_TYPE_SSL_CN,
PP2_TYPE_NETNS,
/**
* A TLV type that is not officially defined in the spec. May be used for nonstandard TLVs
*/
OTHER;
/**
* Returns the the {@link Type} for a specific byte value as defined in the PROXY protocol 1.5 spec
* <p>
* If the byte value is not an official one, it will return {@link Type#OTHER}.
*
* @param byteValue the byte for a type
*
* @return the {@link Type} of a TLV
*/
public static Type typeForByteValue(final byte byteValue) {
switch (byteValue) {
case 0x01:
return PP2_TYPE_ALPN;
case 0x02:
return PP2_TYPE_AUTHORITY;
case 0x20:
return PP2_TYPE_SSL;
case 0x21:
return PP2_TYPE_SSL_VERSION;
case 0x22:
return PP2_TYPE_SSL_CN;
case 0x30:
return PP2_TYPE_NETNS;
default:
return OTHER;
}
}
}
/**
* Creates a new HAProxyTLV
*
* @param type the {@link Type} of the TLV
* @param typeByteValue the byteValue of the TLV. This is especially important if non-standard TLVs are used
* @param content the raw content of the TLV
*/
HAProxyTLV(final Type type, final byte typeByteValue, final ByteBuf content) {
super(content);
checkNotNull(type, "type");
this.type = type;
this.typeByteValue = typeByteValue;
}
/**
* Returns the {@link Type} of this TLV
*/
public Type type() {
return type;
}
/**
* Returns the type of the TLV as byte
*/
public byte typeByteValue() {
return typeByteValue;
}
@Override
public HAProxyTLV copy() {
return replace(content().copy());
}
@Override
public HAProxyTLV duplicate() {
return replace(content().duplicate());
}
@Override
public HAProxyTLV retainedDuplicate() {
return replace(content().retainedDuplicate());
}
@Override
public HAProxyTLV replace(ByteBuf content) {
return new HAProxyTLV(type, typeByteValue, content);
}
@Override
public HAProxyTLV retain() {
super.retain();
return this;
}
@Override
public HAProxyTLV retain(int increment) {
super.retain(increment);
return this;
}
@Override
public HAProxyTLV touch() {
super.touch();
return this;
}
@Override
public HAProxyTLV touch(Object hint) {
super.touch(hint);
return this;
}
}

View File

@ -26,6 +26,8 @@ import io.netty.util.CharsetUtil;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static io.netty.buffer.Unpooled.*;
import static org.junit.Assert.*;
@ -586,6 +588,71 @@ public class HAProxyMessageDecoderTest {
assertFalse(ch.finish());
}
@Test
public void testV2WithSslTLVs() throws Exception {
ch = new EmbeddedChannel(new HAProxyMessageDecoder());
final byte[] bytes = {
13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 35, 127, 0, 0, 1, 127, 0, 0, 1,
-55, -90, 7, 89, 32, 0, 20, 5, 0, 0, 0, 0, 33, 0, 5, 84, 76, 83, 118, 49, 34, 0, 4, 76, 69, 65, 70
};
int startChannels = ch.pipeline().names().size();
assertTrue(ch.writeInbound(copiedBuffer(bytes)));
Object msgObj = ch.readInbound();
assertEquals(startChannels - 1, ch.pipeline().names().size());
HAProxyMessage msg = (HAProxyMessage) msgObj;
assertEquals(HAProxyProtocolVersion.V2, msg.protocolVersion());
assertEquals(HAProxyCommand.PROXY, msg.command());
assertEquals(HAProxyProxiedProtocol.TCP4, msg.proxiedProtocol());
assertEquals("127.0.0.1", msg.sourceAddress());
assertEquals("127.0.0.1", msg.destinationAddress());
assertEquals(51622, msg.sourcePort());
assertEquals(1881, msg.destinationPort());
final List<HAProxyTLV> tlvs = msg.tlvs();
assertEquals(3, tlvs.size());
final HAProxyTLV firstTlv = tlvs.get(0);
assertEquals(HAProxyTLV.Type.PP2_TYPE_SSL, firstTlv.type());
final HAProxySSLTLV sslTlv = (HAProxySSLTLV) firstTlv;
assertEquals(0, sslTlv.verify());
assertTrue(sslTlv.isPP2ClientSSL());
assertTrue(sslTlv.isPP2ClientCertSess());
assertFalse(sslTlv.isPP2ClientCertConn());
final HAProxyTLV secondTlv = tlvs.get(1);
assertEquals(HAProxyTLV.Type.PP2_TYPE_SSL_VERSION, secondTlv.type());
ByteBuf secondContentBuf = secondTlv.content();
byte[] secondContent = new byte[secondContentBuf.readableBytes()];
secondContentBuf.readBytes(secondContent);
assertArrayEquals("TLSv1".getBytes(CharsetUtil.US_ASCII), secondContent);
final HAProxyTLV thirdTLV = tlvs.get(2);
assertEquals(HAProxyTLV.Type.PP2_TYPE_SSL_CN, thirdTLV.type());
ByteBuf thirdContentBuf = thirdTLV.content();
byte[] thirdContent = new byte[thirdContentBuf.readableBytes()];
thirdContentBuf.readBytes(thirdContent);
assertArrayEquals("LEAF".getBytes(CharsetUtil.US_ASCII), thirdContent);
assertTrue(sslTlv.encapsulatedTLVs().contains(secondTlv));
assertTrue(sslTlv.encapsulatedTLVs().contains(thirdTLV));
assertTrue(0 < firstTlv.refCnt());
assertTrue(0 < secondTlv.refCnt());
assertTrue(0 < thirdTLV.refCnt());
assertFalse(thirdTLV.release());
assertFalse(secondTlv.release());
assertTrue(firstTlv.release());
assertEquals(0, firstTlv.refCnt());
assertEquals(0, secondTlv.refCnt());
assertEquals(0, thirdTLV.refCnt());
assertNull(ch.readInbound());
assertFalse(ch.finish());
}
@Test
public void testV2WithTLV() {
ch = new EmbeddedChannel(new HAProxyMessageDecoder(4));

View File

@ -0,0 +1,67 @@
/*
* Copyright 2016 The Netty Project
*
* The Netty Project 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 io.netty.handler.codec.haproxy;
import io.netty.buffer.Unpooled;
import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.*;
public class HAProxySSLTLVTest {
@Test
public void testClientBitmask() throws Exception {
// 0b0000_0111
final byte allClientsEnabled = 0x7;
final HAProxySSLTLV allClientsEnabledTLV =
new HAProxySSLTLV(0, allClientsEnabled, Collections.<HAProxyTLV>emptyList(), Unpooled.buffer());
assertTrue(allClientsEnabledTLV.isPP2ClientCertConn());
assertTrue(allClientsEnabledTLV.isPP2ClientSSL());
assertTrue(allClientsEnabledTLV.isPP2ClientCertSess());
assertTrue(allClientsEnabledTLV.release());
// 0b0000_0101
final byte clientSSLandClientCertSessEnabled = 0x5;
final HAProxySSLTLV clientSSLandClientCertSessTLV =
new HAProxySSLTLV(0, clientSSLandClientCertSessEnabled, Collections.<HAProxyTLV>emptyList(),
Unpooled.buffer());
assertFalse(clientSSLandClientCertSessTLV.isPP2ClientCertConn());
assertTrue(clientSSLandClientCertSessTLV.isPP2ClientSSL());
assertTrue(clientSSLandClientCertSessTLV.isPP2ClientCertSess());
assertTrue(clientSSLandClientCertSessTLV.release());
// 0b0000_0000
final byte noClientEnabled = 0x0;
final HAProxySSLTLV noClientTlv =
new HAProxySSLTLV(0, noClientEnabled, Collections.<HAProxyTLV>emptyList(),
Unpooled.buffer());
assertFalse(noClientTlv.isPP2ClientCertConn());
assertFalse(noClientTlv.isPP2ClientSSL());
assertFalse(noClientTlv.isPP2ClientCertSess());
assertTrue(noClientTlv.release());
}
}