Repository: pires/go-proxyproto
Pull request #10 (2019-08-21 16:36:29)

TLV capture and parsing for proxy protocol v2
 * Adds `rawTLVs` to Header struct
 * API Change
   - non-`io.EOF` errors encountered when reading from payload reader are returned
     from `parseVersion2` and `Read`
   - Padding is saved in `rawTLV`
 * TLV splitting is implemented by `Header.TLVs()`, there is no TLV length validation without
   this explicit call
 * Includes helpers for parsing AWS VPCEs and SSL information
 * Parsing of other types possible by consumers via the `PP2Type` constants using
   the `TLV` type
 * Adds `PP2SSL` exported type with exported fields and helpers for parsing the SSL TLV type.


Patch:
diff --git a/header.go b/header.go
index e2aeee3..6dbaab1 100644
--- a/header.go
+++ b/header.go
@@ -39,6 +39,7 @@ type Header struct {
 	DestinationAddress net.IP
 	SourcePort         uint16
 	DestinationPort    uint16
+	rawTLVs            []byte
 }
 
 // RemoteAddr returns the address of the remote endpoint of the connection.
@@ -71,6 +72,10 @@ func (header *Header) EqualsTo(otherHeader *Header) bool {
 	if header.Command.IsLocal() {
 		return true
 	}
+	// TLVs only exist for version 2
+	if header.Version == 0x02 && !bytes.Equal(header.rawTLVs, otherHeader.rawTLVs) {
+		return false
+	}
 	return header.Version == otherHeader.Version &&
 		header.TransportProtocol == otherHeader.TransportProtocol &&
 		header.SourceAddress.String() == otherHeader.SourceAddress.String() &&
@@ -101,6 +106,11 @@ func (header *Header) Format() ([]byte, error) {
 	}
 }
 
+// TLVs returns the TLVs stored into this header, if they exist.  TLVs are optional for v2 of the protocol.
+func (header *Header) TLVs() ([]TLV, error) {
+	return SplitTLVs(header.rawTLVs)
+}
+
 // Read identifies the proxy protocol version and reads the remaining of
 // the header, accordingly.
 //
diff --git a/tlv.go b/tlv.go
new file mode 100644
index 0000000..bbc86b5
--- /dev/null
+++ b/tlv.go
@@ -0,0 +1,113 @@
+// Type-Length-Value splitting and parsing for proxy protocol V2
+// See spec https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt sections 2.2 to 2.7 and
+
+package proxyproto
+
+import (
+	"encoding/binary"
+	"errors"
+)
+
+const (
+	// Section 2.2
+	PP2_TYPE_ALPN           PP2Type = 0x01
+	PP2_TYPE_AUTHORITY              = 0x02
+	PP2_TYPE_CRC32C                 = 0x03
+	PP2_TYPE_NOOP                   = 0x04
+	PP2_TYPE_SSL                    = 0x20
+	PP2_SUBTYPE_SSL_VERSION         = 0x21
+	PP2_SUBTYPE_SSL_CN              = 0x22
+	PP2_SUBTYPE_SSL_CIPHER          = 0x23
+	PP2_SUBTYPE_SSL_SIG_ALG         = 0x24
+	PP2_SUBTYPE_SSL_KEY_ALG         = 0x25
+	PP2_TYPE_NETNS                  = 0x30
+
+	// Section 2.2.7, reserved types
+	PP2_TYPE_MIN_CUSTOM     = 0xE0
+	PP2_TYPE_MAX_CUSTOM     = 0xEF
+	PP2_TYPE_MIN_EXPERIMENT = 0xF0
+	PP2_TYPE_MAX_EXPERIMENT = 0xF7
+	PP2_TYPE_MIN_FUTURE     = 0xF8
+	PP2_TYPE_MAX_FUTURE     = 0xFF
+)
+
+var (
+	ErrTruncatedTLV    = errors.New("Truncated TLV")
+	ErrMalformedTLV    = errors.New("Malformed TLV Value")
+	ErrIncompatibleTLV = errors.New("Incompatible TLV type")
+)
+
+// PP2Type is the proxy protocol v2 type
+type PP2Type byte
+
+// TLV is a uninterpreted Type-Length-Value for V2 protocol, see section 2.2
+type TLV struct {
+	Type   PP2Type
+	Length int
+	Value  []byte
+}
+
+// SplitTLVs splits the Type-Length-Value vector, returns the vector or an error.
+func SplitTLVs(raw []byte) ([]TLV, error) {
+	var tlvs []TLV
+	for i := 0; i < len(raw); {
+		tlv := TLV{
+			Type: PP2Type(raw[i]),
+		}
+		if len(raw)-i <= 3 {
+			return nil, ErrTruncatedTLV
+		}
+		tlv.Length = int(binary.BigEndian.Uint16(raw[i+1 : i+3])) // Max length = 65K
+		i += 3
+		if i+tlv.Length > len(raw) {
+			return nil, ErrTruncatedTLV
+		}
+		// Ignore no-op padding
+		if tlv.Type != PP2_TYPE_NOOP {
+			tlv.Value = make([]byte, tlv.Length)
+			copy(tlv.Value, raw[i:i+tlv.Length])
+		}
+		i += tlv.Length
+		tlvs = append(tlvs, tlv)
+	}
+	return tlvs, nil
+}
+
+// Registered is true if the type is registered in the spec, see section 2.2
+func (p PP2Type) Registered() bool {
+	switch p {
+	case PP2_TYPE_ALPN,
+		PP2_TYPE_AUTHORITY,
+		PP2_TYPE_CRC32C,
+		PP2_TYPE_NOOP,
+		PP2_TYPE_SSL,
+		PP2_SUBTYPE_SSL_VERSION,
+		PP2_SUBTYPE_SSL_CN,
+		PP2_SUBTYPE_SSL_CIPHER,
+		PP2_SUBTYPE_SSL_SIG_ALG,
+		PP2_SUBTYPE_SSL_KEY_ALG,
+		PP2_TYPE_NETNS:
+		return true
+	}
+	return false
+}
+
+// App is true if the type is reserved for application specific data, see section 2.2.7
+func (p PP2Type) App() bool {
+	return p >= PP2_TYPE_MIN_CUSTOM && p <= PP2_TYPE_MAX_CUSTOM
+}
+
+// Experiment is true if the type is reserved for temporary experimental use by application developers, see section 2.2.7
+func (p PP2Type) Experiment() bool {
+	return p >= PP2_TYPE_MIN_EXPERIMENT && p <= PP2_TYPE_MAX_EXPERIMENT
+}
+
+// Future is true is the type is reserved for future use, see section 2.2.7
+func (p PP2Type) Future() bool {
+	return p >= PP2_TYPE_MIN_FUTURE
+}
+
+// Spec is true if the type is covered by the spec, see section 2.2 and 2.2.7
+func (p PP2Type) Spec() bool {
+	return p.Registered() || p.App() || p.Experiment() || p.Future()
+}
diff --git a/tlvparse/aws.go b/tlvparse/aws.go
new file mode 100644
index 0000000..f47754d
--- /dev/null
+++ b/tlvparse/aws.go
@@ -0,0 +1,43 @@
+// Amazon's application extension to TLVs for NLB VPC endpoint services
+// https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol
+
+package tlvparse
+
+import (
+	"regexp"
+
+	"github.com/pires/go-proxyproto"
+)
+
+const (
+	// Amazon's extension
+	PP2_TYPE_AWS            = 0xEA
+	PP2_SUBTYPE_AWS_VPCE_ID = 0x01
+)
+
+var	vpceRe = regexp.MustCompile("^[A-Za-z0-9-]*$")
+
+func IsAWSVPCEndpointID(tlv proxyproto.TLV) bool {
+	return tlv.Type == PP2_TYPE_AWS && tlv.Length >= 1 && tlv.Value[0] == PP2_SUBTYPE_AWS_VPCE_ID
+}
+
+func AWSVPCEndpointID(tlv proxyproto.TLV) (string, error) {
+	if !IsAWSVPCEndpointID(tlv) {
+		return "", proxyproto.ErrIncompatibleTLV
+	}
+	vpce := string(tlv.Value[1:])
+	if !vpceRe.MatchString(vpce) {
+		return "", proxyproto.ErrMalformedTLV
+	}
+	return vpce, nil
+}
+
+// FindAWSVPCEndpointID returns the first AWS VPC ID in the TLV if it exists and is well-formed.
+func FindAWSVPCEndpointID(tlvs []proxyproto.TLV) string {
+	for _, tlv := range tlvs {
+		if vpc, err := AWSVPCEndpointID(tlv); err == nil && vpc != "" {
+			return vpc
+		}
+	}
+	return ""
+}
diff --git a/tlvparse/ssl.go b/tlvparse/ssl.go
new file mode 100644
index 0000000..e2c2a0e
--- /dev/null
+++ b/tlvparse/ssl.go
@@ -0,0 +1,152 @@
+package tlvparse
+
+import (
+	"encoding/binary"
+	"unicode"
+	"unicode/utf8"
+
+	"github.com/pires/go-proxyproto"
+)
+
+const (
+	// pp2_tlv_ssl.client  bit fields
+	PP2_BITFIELD_CLIENT_SSL       uint8 = 0x01
+	PP2_BITFIELD_CLIENT_CERT_CONN       = 0x02
+	PP2_BITFIELD_CLIENT_CERT_SESS       = 0x04
+
+	tlvSSLMinLen = 5 // len(pp2_tlv_ssl.client) + len(pp2_tlv_ssl.verify)
+)
+
+// 2.2.5. The PP2_TYPE_SSL type and subtypes
+/*
+   struct pp2_tlv_ssl {
+           uint8_t  client;
+           uint32_t verify;
+           struct pp2_tlv sub_tlv[0];
+   };
+*/
+type PP2SSL struct {
+	Client uint8 // The <client> field is made of a bit field from the following values,
+	// indicating which element is present: PP2_BITFIELD_CLIENT_SSL,
+	// PP2_BITFIELD_CLIENT_CERT_CONN, PP2_BITFIELD_CLIENT_CERT_SESS
+	Verify uint32 // Verify will be zero if the client presented a certificate
+	// and it was successfully verified, and non-zero otherwise.
+	TLV []proxyproto.TLV
+}
+
+// Verified is true if the client presented a certificate and it was successfully verified
+func (s PP2SSL) Verified() bool {
+	return s.Verify == 0
+}
+
+// ClientSSL indicates that the client connected over SSL/TLS.  When true, SSLVersion will return the version.
+func (s PP2SSL) ClientSSL() bool {
+	return s.Client&PP2_BITFIELD_CLIENT_SSL == PP2_BITFIELD_CLIENT_SSL
+}
+
+// ClientCertConn indicates that the client provided a certificate over the current connection.
+func (s PP2SSL) ClientCertConn() bool {
+	return s.Client&PP2_BITFIELD_CLIENT_CERT_CONN == PP2_BITFIELD_CLIENT_CERT_CONN
+}
+
+// ClientCertSess indicates that the client provided a certificate at least once over the TLS session this
+// connection belongs to.
+func (s PP2SSL) ClientCertSess() bool {
+	return s.Client&PP2_BITFIELD_CLIENT_CERT_SESS == PP2_BITFIELD_CLIENT_CERT_SESS
+}
+
+// SSLVersion returns the US-ASCII string representation of the TLS version and whether that extension exists.
+func (s PP2SSL) SSLVersion() (string, bool) {
+	for _, tlv := range s.TLV {
+		if tlv.Type == proxyproto.PP2_SUBTYPE_SSL_VERSION {
+			return string(tlv.Value), true
+		}
+	}
+	return "", false
+}
+
+// ClientCN returns the string representation (in UTF8) of the Common Name field (OID: 2.5.4.3) of the client
+// certificate's Distinguished Name and whether that extension exists.
+func (s PP2SSL) ClientCN() (string, bool) {
+	for _, tlv := range s.TLV {
+		if tlv.Type == proxyproto.PP2_SUBTYPE_SSL_CN {
+			return string(tlv.Value), true
+		}
+	}
+	return "", false
+}
+
+// SSLType is true if the TLV is type SSL
+func IsSSL(t proxyproto.TLV) bool {
+	return t.Type == proxyproto.PP2_TYPE_SSL && t.Length >= tlvSSLMinLen
+}
+
+
+// SSL returns the pp2_tlv_ssl from section 2.2.5 or errors with ErrIncompatibleTLV or ErrMalformedTLV
+func SSL(t proxyproto.TLV) (PP2SSL, error) {
+	ssl := PP2SSL{}
+	if !IsSSL(t) {
+		return ssl, proxyproto.ErrIncompatibleTLV
+	}
+	if t.Length < tlvSSLMinLen {
+		return ssl, proxyproto.ErrMalformedTLV
+	}
+	ssl.Client = t.Value[0]
+	ssl.Verify = binary.BigEndian.Uint32(t.Value[1:5])
+	var err error
+	ssl.TLV, err = proxyproto.SplitTLVs(t.Value[5:])
+	if err != nil {
+		return PP2SSL{}, err
+	}
+	versionFound := !ssl.ClientSSL()
+	var cnFound bool
+	for _, tlv := range ssl.TLV {
+		switch tlv.Type {
+		case proxyproto.PP2_SUBTYPE_SSL_VERSION:
+			/*
+				The PP2_CLIENT_SSL flag indicates that the client connected over SSL/TLS. When
+				this field is present, the US-ASCII string representation of the TLS version is
+				appended at the end of the field in the TLV format using the type
+				PP2_SUBTYPE_SSL_VERSION.
+			*/
+			if tlv.Length == 0 || !isASCII(tlv.Value) {
+				return PP2SSL{}, proxyproto.ErrMalformedTLV
+			}
+			versionFound = true
+		case proxyproto.PP2_SUBTYPE_SSL_CN:
+			/*
+				In all cases, the string representation (in UTF8) of the Common Name field
+				(OID: 2.5.4.3) of the client certificate's Distinguished Name, is appended
+				using the TLV format and the type PP2_SUBTYPE_SSL_CN. E.g. "example.com".
+			*/
+			if tlv.Length == 0 || !utf8.Valid(tlv.Value) {
+				return PP2SSL{}, proxyproto.ErrMalformedTLV
+			}
+			cnFound = true
+		}
+	}
+	if !(versionFound && cnFound) {
+		return PP2SSL{}, proxyproto.ErrMalformedTLV
+	}
+	return ssl, nil
+}
+
+// SSL returns the first PP2SSL if it exists and is well formed as well as bool indicating if it was found.
+func FindSSL(tlvs []proxyproto.TLV) (PP2SSL, bool) {
+	for _, t := range tlvs {
+		if ssl, err := SSL(t); err == nil {
+			return ssl, true
+		}
+	}
+	return PP2SSL{}, false
+}
+
+// isASCII checks whether a byte slice has all characters that fit in the ascii character set, including the null byte.
+func isASCII(b []byte) bool {
+	for _, c := range b {
+		if c > unicode.MaxASCII {
+			return false
+		}
+	}
+	return true
+}
diff --git a/v2.go b/v2.go
index e069e02..ed708c7 100644
--- a/v2.go
+++ b/v2.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/binary"
+	"errors"
 	"io"
 )
 
@@ -27,6 +28,7 @@ var (
 		binary.BigEndian.PutUint16(a, lengthUnix)
 		return a
 	}()
+	errUint16Overflow = errors.New("uint16 overflow")
 )
 
 type _ports struct {
@@ -97,7 +99,7 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
 	}
 
 	// Length-limited reader for payload section
-	payloadReader := io.LimitReader(reader, int64(length))
+	payloadReader := io.LimitReader(reader, int64(length)).(*io.LimitedReader)
 
 	// Read addresses and ports
 	if header.TransportProtocol.IsIPv4() {
@@ -134,10 +136,11 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
 	//}
 	//}
 
-	// TODO add encapsulated TLV support
-
-	// Drain the remaining padding
-	payloadReader.Read(make([]byte, length))
+	// Copy bytes for optional Type-Length-Value vector
+	header.rawTLVs = make([]byte, payloadReader.N) // Allocate minimum size slice
+	if _, err = io.ReadFull(payloadReader, header.rawTLVs); err != nil && err != io.EOF {
+		return nil, err
+	}
 
 	return header, nil
 }
@@ -147,14 +150,21 @@ func (header *Header) formatVersion2() ([]byte, error) {
 	buf.Write(SIGV2)
 	buf.WriteByte(header.Command.toByte())
 	buf.WriteByte(header.TransportProtocol.toByte())
-	// TODO add encapsulated TLV length
 	var addrSrc, addrDst []byte
 	if header.TransportProtocol.IsIPv4() {
-		buf.Write(lengthV4Bytes)
+        hdrLen, err := addTLVLen(lengthV4Bytes, len(header.rawTLVs))
+        if err != nil {
+            return nil, err
+        }
+        buf.Write(hdrLen)
 		addrSrc = header.SourceAddress.To4()
 		addrDst = header.DestinationAddress.To4()
 	} else if header.TransportProtocol.IsIPv6() {
-		buf.Write(lengthV6Bytes)
+        hdrLen, err := addTLVLen(lengthV6Bytes, len(header.rawTLVs))
+        if err != nil {
+            return nil, err
+        }
+        buf.Write(hdrLen)
 		addrSrc = header.SourceAddress.To16()
 		addrDst = header.DestinationAddress.To16()
 	} else if header.TransportProtocol.IsUnix() {
@@ -179,6 +189,9 @@ func (header *Header) formatVersion2() ([]byte, error) {
 		return a
 	}()
 	buf.Write(portDstBytes)
+    if len(header.rawTLVs) > 0 {
+        buf.Write(header.rawTLVs)
+    }
 
 	return buf.Bytes(), nil
 }
@@ -193,3 +206,18 @@ func (header *Header) validateLength(length uint16) bool {
 	}
 	return false
 }
+
+// addTLVLen adds the length of the TLV to the header length or errors on uint16 overflow.
+func addTLVLen(cur []byte, tlvLen int) ([]byte, error) {
+	if tlvLen == 0 {
+		return cur, nil
+	}
+	curLen := binary.BigEndian.Uint16(cur)
+	newLen := int(curLen) + tlvLen
+	if newLen >= 1<<16 {
+		return nil, errUint16Overflow
+	}
+	a := make([]byte, 2)
+	binary.BigEndian.PutUint16(a, uint16(newLen))
+	return a, nil
+}


Test patch:
diff --git a/tlv_test.go b/tlv_test.go
new file mode 100644
index 0000000..b1063f1
--- /dev/null
+++ b/tlv_test.go
@@ -0,0 +1,130 @@
+package proxyproto
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/binary"
+	"testing"
+)
+
+var (
+	fixtureOneByteTLV    = []byte{PP2_TYPE_MIN_CUSTOM + 1}
+	fixtureTwoByteTLV    = []byte{PP2_TYPE_MIN_CUSTOM + 2, 0x00}
+	fixtureEmptyLenTLV   = []byte{PP2_TYPE_MIN_CUSTOM + 3, 0x00, 0x01}
+	fixturePartialLenTLV = []byte{PP2_TYPE_MIN_CUSTOM + 3, 0x00, 0x02, 0x00}
+)
+
+func checkTLVs(t *testing.T, name string, raw []byte, expected []PP2Type) []TLV {
+	header, err := parseVersion2(bufio.NewReader(bytes.NewReader(raw)))
+	if err != nil {
+		t.Fatalf("%s: Unexpected error reading header %#v", name, err)
+	}
+
+	tlvs, err := header.TLVs()
+	if err != nil {
+		t.Fatalf("%s: Unexpected error splitting TLVS %#v", name, err)
+	}
+
+	if len(tlvs) != len(expected) {
+		t.Fatalf("%s: Expected %d TLVs, actual %d", name, len(expected), len(tlvs))
+	}
+
+	for i, et := range expected {
+		if at := tlvs[i].Type; at != et {
+			t.Fatalf("%s: Expected type %X, actual %X", name, et, at)
+		}
+	}
+
+	return tlvs
+}
+
+func formatTLV(tlv TLV) []byte {
+	out := make([]byte, 3) // 1 = type + 2 = uint16 length
+	out[0] = byte(tlv.Type)
+	binary.BigEndian.PutUint16(out[1:3], uint16(tlv.Length))
+	return append(out, tlv.Value...)
+}
+
+var invalidTLVTests = []struct {
+	name          string
+	reader        *bufio.Reader
+	expectedError error
+}{
+	{
+		name: "One byte TLV",
+		reader: newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureWithTLV(lengthV4Bytes, fixtureIPv4Address,
+			fixtureOneByteTLV)...)),
+		expectedError: ErrTruncatedTLV,
+	},
+	{
+		name: "Two byte TLV",
+		reader: newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureWithTLV(lengthV4Bytes, fixtureIPv4Address,
+			fixtureTwoByteTLV)...)),
+		expectedError: ErrTruncatedTLV,
+	},
+	{
+		name: "Empty Len TLV",
+		reader: newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureWithTLV(lengthV4Bytes, fixtureIPv4Address,
+			fixtureEmptyLenTLV)...)),
+		expectedError: ErrTruncatedTLV,
+	},
+	{
+		name: "Partial Len TLV",
+		reader: newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureWithTLV(lengthV4Bytes, fixtureIPv4Address,
+			fixturePartialLenTLV)...)),
+		expectedError: ErrTruncatedTLV,
+	},
+}
+
+func TestInvalidV2TLV(t *testing.T) {
+	for _, tc := range invalidTLVTests {
+		t.Run(tc.name, func(t *testing.T) {
+			if hdr, err := Read(tc.reader); err != nil {
+				t.Fatalf("TestInvalidV2TLV %s: unexpected error reading proxy protocol %#v", tc.name, err)
+			} else if _, err := hdr.TLVs(); err != tc.expectedError {
+				t.Fatalf("TestInvalidV2TLV %s: expected %#v, actual %#v", tc.name, tc.expectedError, err)
+			}
+		})
+	}
+}
+
+func TestV2TLVPP2Registered(t *testing.T) {
+	pp2RegTypes := []PP2Type {
+		PP2_TYPE_ALPN, PP2_TYPE_AUTHORITY, PP2_TYPE_CRC32C, PP2_TYPE_NOOP,
+		PP2_TYPE_SSL, PP2_SUBTYPE_SSL_VERSION, PP2_SUBTYPE_SSL_CN,
+		PP2_SUBTYPE_SSL_CIPHER, PP2_SUBTYPE_SSL_SIG_ALG, PP2_SUBTYPE_SSL_KEY_ALG,
+		PP2_TYPE_NETNS,
+	}
+	pp2RegMap := make(map[PP2Type]bool)
+	for _, p := range pp2RegTypes {
+		pp2RegMap[p] = true
+		if !p.Registered() {
+			t.Fatalf("TestV2TLVPP2Registered: type %x should be registered", p)
+		}
+		if !p.Spec() {
+			t.Fatalf("TestV2TLVPP2Registered: type %x should be in spec", p)
+		}
+		if p.App() {
+			t.Fatalf("TestV2TLVPP2Registered: type %x unexpectedly app", p)
+		}
+		if p.Experiment() {
+			t.Fatalf("TestV2TLVPP2Registered: type %x unexpectedly experiment", p)
+		}
+		if p.Future() {
+			t.Fatalf("TestV2TLVPP2Registered: type %x unexpectedly future", p)
+		}
+	}
+
+	lastType := PP2Type(0xFF)
+	for i := PP2Type(0x00); i < lastType; i++ {
+		if !pp2RegMap[i] {
+			if i.Registered() {
+				t.Fatalf("TestV2TLVPP2Registered: type %x unexpectedly registered", i)
+			}
+		}
+	}
+
+	if lastType.Registered() {
+		t.Fatalf("TestV2TLVPP2Registered: type %x unexpectedly registered", lastType)
+	}
+}
diff --git a/tlvparse/aws_test.go b/tlvparse/aws_test.go
new file mode 100644
index 0000000..cf0c8cc
--- /dev/null
+++ b/tlvparse/aws_test.go
@@ -0,0 +1,234 @@
+package tlvparse
+
+import (
+	"encoding/binary"
+	"github.com/pires/go-proxyproto"
+	"testing"
+)
+
+var awsTestCases = []struct {
+	name  string
+	raw   []byte
+	types []proxyproto.PP2Type
+	valid func(*testing.T, string, []proxyproto.TLV)
+}{
+	{
+		name: "VPCE example",
+		// https://github.com/aws/elastic-load-balancing-tools/blob/c8eee30ab991ab4c57dc37d1c58f09f67bd534aa/proprot/tst/com/amazonaws/proprot/Compatibility_AwsNetworkLoadBalancerTest.java#L41..L67
+		raw: []byte{
+			0x0d, 0x0a, 0x0d, 0x0a, /* Start of Sig */
+			0x00, 0x0d, 0x0a, 0x51,
+			0x55, 0x49, 0x54, 0x0a, /* End of Sig */
+			0x21, 0x11, 0x00, 0x54, /* ver_cmd, fam and len */
+			0xac, 0x1f, 0x07, 0x71, /* Caller src ip */
+			0xac, 0x1f, 0x0a, 0x1f, /* Endpoint dst ip */
+			0xc8, 0xf2, 0x00, 0x50, /* Proxy src port & dst port */
+			0x03, 0x00, 0x04, 0xe8, /* CRC TLV start */
+			0xd6, 0x89, 0x2d, 0xea, /* CRC TLV cont, VPCE id TLV start */
+			0x00, 0x17, 0x01, 0x76,
+			0x70, 0x63, 0x65, 0x2d,
+			0x30, 0x38, 0x64, 0x32,
+			0x62, 0x66, 0x31, 0x35,
+			0x66, 0x61, 0x63, 0x35,
+			0x30, 0x30, 0x31, 0x63,
+			0x39, 0x04, 0x00, 0x24, /* VPCE id TLV end, NOOP TLV start*/
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, /* NOOP TLV end */
+		},
+		types: []proxyproto.PP2Type{proxyproto.PP2_TYPE_CRC32C, PP2_TYPE_AWS, proxyproto.PP2_TYPE_NOOP},
+		valid: func(t *testing.T, name string, tlvs []proxyproto.TLV) {
+			if !IsAWSVPCEndpointID(tlvs[1]) {
+				t.Fatalf("TestParseV2TLV %s: Expected tlvs[1] to be an AWSVPCEndpointID type", name)
+			}
+
+			vpce := "vpce-08d2bf15fac5001c9"
+			if vpca, err := AWSVPCEndpointID(tlvs[1]); err != nil {
+				t.Fatalf("TestParseV2TLV %s: Unexpected error when parsing AWSVPCEndpointID", name)
+			} else if vpca != vpce {
+				t.Fatalf("TestParseV2TLV %s: Unexpected VPC ID from tlvs[1] expected %#v, actual %#v", name, vpce, vpca)
+			}
+
+			if vpca := FindAWSVPCEndpointID(tlvs); vpca == "" {
+				t.Fatalf("TestParseV2TLV %s: Expected to find AWSVPCEndpointID %#v in TLVs", name, vpce)
+			} else if vpca != vpce {
+				t.Fatalf("TestParseV2TLV %s: Unexpected AWSVPCEndpointID from header expected %#v, actual %#v", name, vpce, vpca)
+			}
+
+		},
+	},
+	{
+		name: "VPCE capture",
+		raw: []byte{
+			0x0d, 0x0a, 0x0d, 0x0a,
+			0x00, 0x0d, 0x0a, 0x51,
+			0x55, 0x49, 0x54, 0x0a,
+			0x21, 0x11, 0x00, 0x54,
+			0xc0, 0xa8, 0x2c, 0x0a,
+			0xc0, 0xa8, 0x2c, 0x07,
+			0xcc, 0x3e, 0x24, 0x1b,
+			0x03, 0x00, 0x04, 0xb9,
+			0x28, 0x6f, 0xa6, 0xea,
+			0x00, 0x17, 0x01, 0x76,
+			0x70, 0x63, 0x65, 0x2d,
+			0x30, 0x30, 0x65, 0x61,
+			0x66, 0x63, 0x34, 0x35,
+			0x38, 0x65, 0x63, 0x39,
+			0x37, 0x62, 0x38, 0x33,
+			0x33, 0x04, 0x00, 0x24,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+		},
+		types: []proxyproto.PP2Type{proxyproto.PP2_TYPE_CRC32C, PP2_TYPE_AWS, proxyproto.PP2_TYPE_NOOP},
+		valid: func(t *testing.T, name string, tlvs []proxyproto.TLV) {
+			if !IsAWSVPCEndpointID(tlvs[1]) {
+				t.Fatalf("TestParseV2TLV %s: Expected tlvs[1] to be an AWS VPC endpoint ID type", name)
+			}
+
+			vpce := "vpce-00eafc458ec97b833"
+			if vpca, err := AWSVPCEndpointID(tlvs[1]); err != nil {
+				t.Fatalf("TestParseV2TLV %s: Unexpected error when parsing AWS VPC ID", name)
+			} else if vpca != vpce {
+				t.Fatalf("TestParseV2TLV %s: Unexpected VPC ID from tlvs[1] expected %#v, actual %#v", name, vpce, vpca)
+			}
+
+			if vpca := FindAWSVPCEndpointID(tlvs); vpca == "" {
+				t.Fatalf("TestParseV2TLV %s: Expected to find VPC ID %#v in TLVs", name, vpce)
+			} else if vpca != vpce {
+				t.Fatalf("TestParseV2TLV %s: Unexpected VPC ID from header expected %#v, actual %#v", name, vpce, vpca)
+			}
+
+		},
+	},
+}
+
+func TestV2TLVAWSVPCEBadChars(t *testing.T) {
+	badVPCE := "vcpe-!?***&&&&&&&"
+	rawTLVs := vpceTLV(badVPCE)
+	tlvs, err := proxyproto.SplitTLVs(rawTLVs)
+	if len(tlvs) != 1 {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected TLV length expected: %#v, actual: %#v", 1, tlvs)
+	}
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected TLV parsing error %#v", err)
+	}
+
+	_, err = AWSVPCEndpointID(tlvs[0])
+	if err != proxyproto.ErrMalformedTLV {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected error actual: %#v", err)
+	}
+
+	if FindAWSVPCEndpointID(tlvs) != "" {
+		t.Fatal("TestV2TLVAWSVPCEBadChars: AWSVPCEndpointID unexpectedly found")
+	}
+
+	rawTLVs = vpceTLV("")
+	tlvs, err = proxyproto.SplitTLVs(rawTLVs)
+	if len(tlvs) != 1 {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected TLV length expected: %#v, actual: %#v", 1, tlvs)
+	}
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected TLV parsing error %#v", err)
+	}
+
+	parsedVPCE, err := AWSVPCEndpointID(tlvs[0])
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: unexpected error actual: %#v", err)
+	}
+
+	if parsedVPCE != "" {
+		t.Fatalf("TestV2TLVAWSVPCEBadChars: found non-empty vpce, actual: %#v", parsedVPCE)
+	}
+
+	parsedVPCE = FindAWSVPCEndpointID(tlvs)
+	if parsedVPCE != "" {
+		t.Fatal("TestV2TLVAWSVPCEBadChars: AWSVPECID unexpectedly found")
+	}
+}
+
+func TestParseAWSVPCEndpointIDTLVs(t *testing.T) {
+	for _, tc := range awsTestCases {
+		t.Run(tc.name, func(t *testing.T) {
+			tlvs := checkTLVs(t, tc.name, tc.raw, tc.types)
+			tc.valid(t, tc.name, tlvs)
+		})
+	}
+}
+
+func TestV2TLVAWSUnknownSubtype(t *testing.T) {
+	vpce := "vpce-abc1234"
+
+	rawTLVs := vpceTLV(vpce)
+	tlvs, err := proxyproto.SplitTLVs(rawTLVs)
+	if len(tlvs) != 1 {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected TLV length expected: %#v, actual: %#v", 1, tlvs)
+	}
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected TLV parsing error %#v", err)
+	}
+
+	avpce, err := AWSVPCEndpointID(tlvs[0])
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected AWSVPCEndpointID error actual: %#v", err)
+	}
+	if avpce != vpce {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected vpce value expected: %#v, actual: %#v", vpce, avpce)
+	}
+	avpce = FindAWSVPCEndpointID(tlvs)
+	if avpce == "" {
+		t.Fatal("TestV2TLVAWSUnknownSubtype: AWSVPCEndpointID unexpectedly missing")
+	}
+	if avpce != vpce {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected AWSVPCEndpointID value expected: %#v, actual: %#v", vpce, avpce)
+	}
+
+	subtypeIndex := 3
+	// Sanity check
+	if rawTLVs[subtypeIndex] != PP2_SUBTYPE_AWS_VPCE_ID {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected subtype expected %x, actual %x", PP2_SUBTYPE_AWS_VPCE_ID, rawTLVs[subtypeIndex])
+	}
+
+	rawTLVs[subtypeIndex] = PP2_SUBTYPE_AWS_VPCE_ID + 1
+
+	tlvs, err = proxyproto.SplitTLVs(rawTLVs)
+	if len(tlvs) != 1 {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected TLV length expected: %#v, actual: %#v", 1, tlvs)
+	}
+	if err != nil {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected TLV parsing error %#v", err)
+	}
+
+	if IsAWSVPCEndpointID(tlvs[0]) {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: AWSVPCEType() unexpectedly true after changing subtype")
+	}
+
+	_, err = AWSVPCEndpointID(tlvs[0])
+	if err != proxyproto.ErrIncompatibleTLV {
+		t.Fatalf("TestV2TLVAWSUnknownSubtype: unexpected AWSVPCEndpointID error expected %#v, actual: %#v", proxyproto.ErrIncompatibleTLV, err)
+	}
+
+	if FindAWSVPCEndpointID(tlvs) != "" {
+		t.Fatal("TestV2TLVAWSUnknownSubtype: AWSVPCEndpointID unexpectedly exists despite invalid subtype")
+	}
+}
+
+func vpceTLV(vpce string) []byte {
+	tlv := []byte{
+		PP2_TYPE_AWS, 0x00, 0x00, PP2_SUBTYPE_AWS_VPCE_ID,
+	}
+	binary.BigEndian.PutUint16(tlv[1:3], uint16(len(vpce) + 1)) // +1 for subtype
+	return append(tlv, []byte(vpce)...)
+}
diff --git a/tlvparse/ssl_test.go b/tlvparse/ssl_test.go
new file mode 100644
index 0000000..1ffd4a4
--- /dev/null
+++ b/tlvparse/ssl_test.go
@@ -0,0 +1,91 @@
+package tlvparse
+
+import (
+	"testing"
+
+	"github.com/pires/go-proxyproto"
+)
+
+var testCases = []struct {
+	name  string
+	raw   []byte
+	types []proxyproto.PP2Type
+	valid func(*testing.T, string, []proxyproto.TLV)
+}{
+	{
+		name: "SSL haproxy cn",
+		raw: []byte{
+			0x0d, 0x0a, 0x0d, 0x0a,
+			0x00, 0x0d, 0x0a, 0x51,
+			0x55, 0x49, 0x54, 0x0a,
+			0x21, 0x11, 0x00, 0x40,
+			0x7f, 0x00, 0x00, 0x01,
+			0x7f, 0x00, 0x00, 0x01,
+			0xcc, 0x8a, 0x23, 0x2e,
+			0x20, 0x00, 0x31, 0x07,
+			0x00, 0x00, 0x00, 0x00,
+			0x21, 0x00, 0x07, 0x54,
+			0x4c, 0x53, 0x76, 0x31,
+			0x2e, 0x33, 0x22, 0x00,
+			0x1f, 0x45, 0x78, 0x61,
+			0x6d, 0x70, 0x6c, 0x65,
+			0x20, 0x43, 0x6f, 0x6d,
+			0x6d, 0x6f, 0x6e, 0x20,
+			0x4e, 0x61, 0x6d, 0x65,
+			0x20, 0x43, 0x6c, 0x69,
+			0x65, 0x6e, 0x74, 0x20,
+			0x43, 0x65, 0x72, 0x74,
+		},
+		types: []proxyproto.PP2Type{proxyproto.PP2_TYPE_SSL},
+		valid: func(t *testing.T, name string, tlvs []proxyproto.TLV) {
+			if !IsSSL(tlvs[0]) {
+				t.Fatalf("TestParseV2TLV %s: Expected tlvs[0] to be the SSL type", name)
+			}
+
+			ssl, err := SSL(tlvs[0])
+			if err != nil {
+				t.Fatalf("TestParseV2TLV %s: Unexpected error when parsing SSL %#v", name, err)
+			}
+
+			if !ssl.ClientSSL() {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientSSL() to be true", name)
+			}
+
+			if !ssl.ClientCertConn() {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientCertConn() to be true", name)
+			}
+
+			if !ssl.ClientCertSess() {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientCertSess() to be true", name)
+			}
+
+			ecn := "Example Common Name Client Cert"
+			if acn, ok := ssl.ClientCN(); !ok {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientCN to exist", name)
+			} else if acn != ecn {
+				t.Fatalf("TestParseV2TLV %s: Unexpected ClientCN expected %#v, actual %#v", name, ecn, acn)
+			}
+
+			esslVer := "TLSv1.3"
+			if asslVer, ok := ssl.SSLVersion(); !ok {
+				t.Fatalf("TestParseV2TLV %s: Expected SSLVersion to exist", name)
+			} else if asslVer != esslVer {
+				t.Fatalf("TestParseV2TLV %s: Unexpected SSLVersion expected %#v, actual %#v", name, esslVer, asslVer)
+			}
+
+			if !ssl.Verified() {
+				t.Fatalf("TestParseV2TLV %s: Expected Verified to be true", name)
+			}
+		},
+	},
+}
+
+func TestParseV2TLV(t *testing.T) {
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			tlvs := checkTLVs(t, tc.name, tc.raw, tc.types)
+			tc.valid(t, tc.name, tlvs)
+		})
+	}
+}
+
diff --git a/tlvparse/test.go b/tlvparse/test.go
new file mode 100644
index 0000000..ca9c53d
--- /dev/null
+++ b/tlvparse/test.go
@@ -0,0 +1,33 @@
+package tlvparse
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"github.com/pires/go-proxyproto"
+)
+
+func checkTLVs(t *testing.T, name string, raw []byte, expected []proxyproto.PP2Type) []proxyproto.TLV {
+	header, err := proxyproto.Read(bufio.NewReader(bytes.NewReader(raw)))
+	if err != nil {
+		t.Fatalf("%s: Unexpected error reading header %#v", name, err)
+	}
+
+	tlvs, err := header.TLVs()
+	if err != nil {
+		t.Fatalf("%s: Unexpected error splitting TLVS %#v", name, err)
+	}
+
+	if len(tlvs) != len(expected) {
+		t.Fatalf("%s: Expected %d TLVs, actual %d", name, len(expected), len(tlvs))
+	}
+
+	for i, et := range expected {
+		if at := tlvs[i].Type; at != et {
+			t.Fatalf("%s: Expected type %X, actual %X", name, et, at)
+		}
+	}
+
+	return tlvs
+}
diff --git a/v2_test.go b/v2_test.go
index 123d1ad..7b44b12 100644
--- a/v2_test.go
+++ b/v2_test.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/binary"
+	"math/rand"
 	"reflect"
 	"testing"
 )
@@ -44,6 +45,13 @@ var (
 	fixtureIPv6Address  = append(addressesIPv6, ports...)
 	fixtureIPv6V2       = append(lengthV6Bytes, fixtureIPv6Address...)
 	fixtureIPv6V2Padded = append(append(lengthPaddedBytes, fixtureIPv6Address...), make([]byte, lengthPadded-lengthV6)...)
+	fixtureTLV          = func() []byte {
+		tlv := make([]byte, 2+rand.Intn(1<<12)) // Not enough to overflow, at least size two
+		rand.Read(tlv)
+		return tlv
+	}()
+	fixtureIPv4V2TLV = fixtureWithTLV(lengthV4Bytes, fixtureIPv4Address, fixtureTLV)
+	fixtureIPv6V2TLV = fixtureWithTLV(lengthV6Bytes, fixtureIPv6Address, fixtureTLV)
 
 	// Arbitrary bytes following proxy bytes
 	arbitraryTailBytes = []byte{'\x99', '\x97', '\x98'}
@@ -154,6 +162,34 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationPort:    PORT,
 		},
 	},
+	// PROXY TCP IPv4 with TLV
+	{
+		newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureIPv4V2TLV...)),
+		&Header{
+			Version:            2,
+			Command:            PROXY,
+			TransportProtocol:  TCPv4,
+			SourceAddress:      v4addr,
+			DestinationAddress: v4addr,
+			SourcePort:         PORT,
+			DestinationPort:    PORT,
+			rawTLVs:            fixtureTLV,
+		},
+	},
+	// PROXY TCP IPv6 with TLV
+	{
+		newBufioReader(append(append(SIGV2, PROXY, TCPv6), fixtureIPv6V2TLV...)),
+		&Header{
+			Version:            2,
+			Command:            PROXY,
+			TransportProtocol:  TCPv6,
+			SourceAddress:      v6addr,
+			DestinationAddress: v6addr,
+			SourcePort:         PORT,
+			DestinationPort:    PORT,
+			rawTLVs:            fixtureTLV,
+		},
+	},
 	// PROXY UDP IPv4
 	{
 		newBufioReader(append(append(SIGV2, PROXY, UDPv4), fixtureIPv4V2...)),
@@ -232,6 +268,7 @@ var validParseV2PaddedTests = []struct {
 			DestinationAddress: v4addr,
 			SourcePort:         PORT,
 			DestinationPort:    PORT,
+			rawTLVs:            make([]byte, lengthPadded-lengthV4),
 		},
 	},
 	// PROXY TCP IPv6
@@ -245,6 +282,7 @@ var validParseV2PaddedTests = []struct {
 			DestinationAddress: v6addr,
 			SourcePort:         PORT,
 			DestinationPort:    PORT,
+			rawTLVs:            make([]byte, lengthPadded-lengthV6),
 		},
 	},
 	// PROXY UDP IPv4
@@ -258,6 +296,7 @@ var validParseV2PaddedTests = []struct {
 			DestinationAddress: v4addr,
 			SourcePort:         PORT,
 			DestinationPort:    PORT,
+			rawTLVs:            make([]byte, lengthPadded-lengthV4),
 		},
 	},
 	// PROXY UDP IPv6
@@ -271,6 +310,7 @@ var validParseV2PaddedTests = []struct {
 			DestinationAddress: v6addr,
 			SourcePort:         PORT,
 			DestinationPort:    PORT,
+			rawTLVs:            make([]byte, lengthPadded-lengthV6),
 		},
 	},
 }
@@ -298,6 +338,98 @@ func TestParseV2Padded(t *testing.T) {
 	}
 }
 
+func TestV2EqualsToTLV(t *testing.T) {
+	eHdr := &Header{
+		Version:            2,
+		Command:            PROXY,
+		TransportProtocol:  TCPv4,
+		SourceAddress:      v4addr,
+		DestinationAddress: v4addr,
+		SourcePort:         PORT,
+		DestinationPort:    PORT,
+	}
+	hdr, err := Read(newBufioReader(append(append(SIGV2, PROXY, TCPv4), fixtureIPv4V2TLV...)))
+	if err != nil {
+		t.Fatal("TestV2EqualsToTLV: Unexpected error ", err)
+	}
+	if eHdr.EqualsTo(hdr) {
+		t.Fatalf("TestV2EqualsToTLV: Unexpectedly equal created: %#v, parsed: %#v", eHdr, hdr)
+	}
+	eHdr.rawTLVs = fixtureTLV[:]
+
+	if !eHdr.EqualsTo(hdr) {
+		t.Fatalf("TestV2EqualsToTLV: Unexpectedly unequal after tlv copy created: %#v, parsed: %#v", eHdr, hdr)
+	}
+
+	eHdr.rawTLVs[0] = eHdr.rawTLVs[0] + 1
+	if eHdr.EqualsTo(hdr) {
+		t.Fatalf("TestV2EqualsToTLV: Unexpectedly equal after changing tlv created: %#v, parsed: %#v", eHdr, hdr)
+	}
+}
+
+var tlvFormatTests = []*Header{
+	// PROXY TCP IPv4
+	&Header{
+		Version:            2,
+		Command:            PROXY,
+		TransportProtocol:  TCPv4,
+		SourceAddress:      v4addr,
+		DestinationAddress: v4addr,
+		SourcePort:         PORT,
+		DestinationPort:    PORT,
+		rawTLVs:            make([]byte, 1<<16),
+	},
+	// PROXY TCP IPv6
+	&Header{
+		Version:            2,
+		Command:            PROXY,
+		TransportProtocol:  TCPv6,
+		SourceAddress:      v6addr,
+		DestinationAddress: v6addr,
+		SourcePort:         PORT,
+		DestinationPort:    PORT,
+		rawTLVs:            make([]byte, 1<<16),
+	},
+	// PROXY UDP IPv4
+	&Header{
+		Version:            2,
+		Command:            PROXY,
+		TransportProtocol:  UDPv4,
+		SourceAddress:      v4addr,
+		DestinationAddress: v4addr,
+		SourcePort:         PORT,
+		DestinationPort:    PORT,
+		rawTLVs:            make([]byte, 1<<16),
+	},
+	// PROXY UDP IPv6
+	&Header{
+		Version:            2,
+		Command:            PROXY,
+		TransportProtocol:  UDPv6,
+		SourceAddress:      v6addr,
+		DestinationAddress: v6addr,
+		SourcePort:         PORT,
+		DestinationPort:    PORT,
+		rawTLVs:            make([]byte, 1<<16),
+	},
+}
+
+func TestV2TLVFormatTooLargeTLV(t *testing.T) {
+	for _, tt := range tlvFormatTests {
+		if _, err := tt.Format(); err != errUint16Overflow {
+			t.Fatalf("TestV2TLVFormatTooLargeTLV: missing or expected error when formatting too-large TLV %#v", err)
+		}
+	}
+}
+
 func newBufioReader(b []byte) *bufio.Reader {
 	return bufio.NewReader(bytes.NewReader(b))
 }
+
+func fixtureWithTLV(cur []byte, addr []byte, tlv []byte) []byte {
+	tlen, err := addTLVLen(cur, len(tlv))
+	if err != nil {
+		panic(err)
+	}
+	return append(append(tlen, addr...), tlv...)
+}