Repository: pires/go-proxyproto (go)

Issue:
Parsing v2 UNSPEC and v1 PROXY UNKNOWN not implemented
Hi! I found that this library is missing the ability to parse these two kinds of header, which are required by the protocol and mandatory to implement, while trying to resolve XTLS/Xray-core#166

### v2 UNSPEC

When this header is received:

```
0d 0a 0d 0a 00 0d 0a 51 55 49 54 0a 20 00 00 00
```

which is <v2 signature> + LOCAL command (\x20) + UNSPEC address family UNSPEC protocol (\x00)
`ErrUnsupportedAddressFamilyAndProtocol` is returned
https://github.com/pires/go-proxyproto/blob/adbbabed1f24a2123e1d4368ae8da8378b347643/v2.go#L85-L87
This is because UNSPEC is absent in supportedTransportProtocol 
https://github.com/pires/go-proxyproto/blob/adbbabed1f24a2123e1d4368ae8da8378b347643/addr_proto.go#L6-L23
This UNSPEC header is used when HAProxy doing health checks, or the proxied client is requesting via a unix socket.
Also the protocol spec says:

```
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.
```

### v1 PROXY UNKNOWN

When header `PROXY UNKNOWN\r\n` is received,
https://github.com/pires/go-proxyproto/blob/adbbabed1f24a2123e1d4368ae8da8378b347643/v1.go#L25-L33
returns ErrCantReadProtocolVersionAndCommand.
This is because in this case, there's only 2 parts separated by spaces, instead of 6 parts as `PROXY <PROTO> <SRC_ADDR> <DEST_ADDR> <SRC_PORT> <DEST_PORT>`
The protocol spec says:
```
If the announced transport protocol is "UNKNOWN", then the receiver knows that
the sender speaks the correct PROXY protocol with the appropriate version, and
SHOULD accept the connection and use the real connection's parameters as if
there were no PROXY protocol header on the wire.
```
Also, this kind of header can be formatted by the library.
https://github.com/pires/go-proxyproto/blob/adbbabed1f24a2123e1d4368ae8da8378b347643/v1.go#L76-L88

Although I really don't know much about Golang, I'll try my best to support you to resolve this issue. Thanks!

Pull request description:
Support v1 UNKNOWN and v2 UNSPEC when command is LOCAL
Fixes #60

I took the opportunity to improve test debuggability for v1 and v2 code.

Warning: changes to v1 errror handling may break someone's code but should be trivial to address. I'm taking this breaking change lightly because of that and because this is still a 0.x release, which should clearly communicate the API is not stable.

Patch:
diff --git a/addr_proto.go b/addr_proto.go
index 0d0bc70..687b75d 100644
--- a/addr_proto.go
+++ b/addr_proto.go
@@ -13,15 +13,6 @@ const (
 	UnixDatagram AddressFamilyAndProtocol = '\x32'
 )
 
-var supportedTransportProtocol = map[AddressFamilyAndProtocol]bool{
-	TCPv4:        true,
-	UDPv4:        true,
-	TCPv6:        true,
-	UDPv6:        true,
-	UnixStream:   true,
-	UnixDatagram: true,
-}
-
 // IsIPv4 returns true if the address family is IPv4 (AF_INET4), false otherwise.
 func (ap AddressFamilyAndProtocol) IsIPv4() bool {
 	return 0x10 == ap&0xF0
diff --git a/header.go b/header.go
index f22bbc2..48d3fa7 100644
--- a/header.go
+++ b/header.go
@@ -16,6 +16,7 @@ var (
 	SIGV1 = []byte{'\x50', '\x52', '\x4F', '\x58', '\x59'}
 	SIGV2 = []byte{'\x0D', '\x0A', '\x0D', '\x0A', '\x00', '\x0D', '\x0A', '\x51', '\x55', '\x49', '\x54', '\x0A'}
 
+	ErrLineMustEndWithCrlf                  = errors.New("proxyproto: header is invalid, must end with \\r\\n")
 	ErrCantReadProtocolVersionAndCommand    = errors.New("proxyproto: can't read proxy protocol version and command")
 	ErrCantReadAddressFamilyAndProtocol     = errors.New("proxyproto: can't read address family or protocol")
 	ErrCantReadLength                       = errors.New("proxyproto: can't read length")
diff --git a/protocol.go b/protocol.go
index 9fb2df1..878ca1c 100644
--- a/protocol.go
+++ b/protocol.go
@@ -157,7 +157,7 @@ func (p *Conn) RemoteAddr() net.Addr {
 // Raw returns the underlying connection which can be casted to
 // a concrete type, allowing access to specialized functions.
 //
-// Use this ONLY if you know exactly what you are doing. 
+// Use this ONLY if you know exactly what you are doing.
 func (p *Conn) Raw() net.Conn {
 	return p.conn
 }
@@ -165,7 +165,7 @@ func (p *Conn) Raw() net.Conn {
 // TCPConn returns the underlying TCP connection,
 // allowing access to specialized functions.
 //
-// Use this ONLY if you know exactly what you are doing. 
+// Use this ONLY if you know exactly what you are doing.
 func (p *Conn) TCPConn() (conn *net.TCPConn, ok bool) {
 	conn, ok = p.conn.(*net.TCPConn)
 	return
@@ -174,7 +174,7 @@ func (p *Conn) TCPConn() (conn *net.TCPConn, ok bool) {
 // UnixConn returns the underlying Unix socket connection,
 // allowing access to specialized functions.
 //
-// Use this ONLY if you know exactly what you are doing. 
+// Use this ONLY if you know exactly what you are doing.
 func (p *Conn) UnixConn() (conn *net.UnixConn, ok bool) {
 	conn, ok = p.conn.(*net.UnixConn)
 	return
@@ -183,7 +183,7 @@ func (p *Conn) UnixConn() (conn *net.UnixConn, ok bool) {
 // UDPConn returns the underlying UDP connection,
 // allowing access to specialized functions.
 //
-// Use this ONLY if you know exactly what you are doing. 
+// Use this ONLY if you know exactly what you are doing.
 func (p *Conn) UDPConn() (conn *net.UDPConn, ok bool) {
 	conn, ok = p.conn.(*net.UDPConn)
 	return
diff --git a/v1.go b/v1.go
index 059faf9..0020d79 100644
--- a/v1.go
+++ b/v1.go
@@ -22,52 +22,73 @@ func initVersion1() *Header {
 }
 
 func parseVersion1(reader *bufio.Reader) (*Header, error) {
-	// Make sure we have a v1 header
+	// Read until LF shows up, otherwise fail.
+	// At this point, can't be sure CR precedes LF which will be validated next.
 	line, err := reader.ReadString('\n')
+	if err != nil {
+		return nil, ErrLineMustEndWithCrlf
+	}
 	if !strings.HasSuffix(line, crlf) {
-		return nil, ErrCantReadProtocolVersionAndCommand
+		return nil, ErrLineMustEndWithCrlf
 	}
+	// Check full signature.
 	tokens := strings.Split(line[:len(line)-2], separator)
-	if len(tokens) < 6 {
-		return nil, ErrCantReadProtocolVersionAndCommand
+	transportProtocol := UNSPEC // doesn't exist in v1 but fits UNKNOWN.
+	if len(tokens) > 0 {
+		// Read address family and protocol
+		switch tokens[1] {
+		case "TCP4":
+			transportProtocol = TCPv4
+		case "TCP6":
+			transportProtocol = TCPv6
+		case "UNKNOWN": // no-op as UNSPEC is set already
+		default:
+			return nil, ErrCantReadAddressFamilyAndProtocol
+		}
+
+		// Expect 6 tokens only when UNKNOWN is not present.
+		if !transportProtocol.IsUnspec() && len(tokens) < 6 {
+			return nil, ErrCantReadAddressFamilyAndProtocol
+		}
 	}
 
+	// Allocation only happens when a signature is found.
 	header := initVersion1()
+	// If UNKNOWN is present, set Command to LOCAL.
+	// Command is not present in v1 but set it for other parts of
+	// this library to rely on it for determining connection details.
+	header.Command = LOCAL
 
-	// Read address family and protocol
-	switch tokens[1] {
-	case "TCP4":
-		header.TransportProtocol = TCPv4
-	case "TCP6":
-		header.TransportProtocol = TCPv6
-	default:
-		header.TransportProtocol = UNSPEC
-	}
+	// Transport protocol has been processed already.
+	header.TransportProtocol = transportProtocol
 
-	// Read addresses and ports
-	sourceIP, err := parseV1IPAddress(header.TransportProtocol, tokens[2])
-	if err != nil {
-		return nil, err
-	}
-	destIP, err := parseV1IPAddress(header.TransportProtocol, tokens[3])
-	if err != nil {
-		return nil, err
-	}
-	sourcePort, err := parseV1PortNumber(tokens[4])
-	if err != nil {
-		return nil, err
-	}
-	destPort, err := parseV1PortNumber(tokens[5])
-	if err != nil {
-		return nil, err
-	}
-	header.SourceAddr = &net.TCPAddr{
-		IP:   sourceIP,
-		Port: sourcePort,
-	}
-	header.DestinationAddr = &net.TCPAddr{
-		IP:   destIP,
-		Port: destPort,
+	// Only process further if UNKNOWN is not present.
+	if header.TransportProtocol != UNSPEC {
+		// Read addresses and ports
+		sourceIP, err := parseV1IPAddress(header.TransportProtocol, tokens[2])
+		if err != nil {
+			return nil, err
+		}
+		destIP, err := parseV1IPAddress(header.TransportProtocol, tokens[3])
+		if err != nil {
+			return nil, err
+		}
+		sourcePort, err := parseV1PortNumber(tokens[4])
+		if err != nil {
+			return nil, err
+		}
+		destPort, err := parseV1PortNumber(tokens[5])
+		if err != nil {
+			return nil, err
+		}
+		header.SourceAddr = &net.TCPAddr{
+			IP:   sourceIP,
+			Port: sourcePort,
+		}
+		header.DestinationAddr = &net.TCPAddr{
+			IP:   destIP,
+			Port: destPort,
+		}
 	}
 
 	return header, nil
@@ -84,7 +105,7 @@ func (header *Header) formatVersion1() ([]byte, error) {
 		proto = "TCP6"
 	default:
 		// Unknown connection (short form)
-		return []byte("PROXY UNKNOWN\r\n"), nil
+		return []byte("PROXY UNKNOWN" + crlf), nil
 	}
 
 	sourceAddr, sourceOK := header.SourceAddr.(*net.TCPAddr)
diff --git a/v2.go b/v2.go
index 7c281e3..b0fdd58 100644
--- a/v2.go
+++ b/v2.go
@@ -82,8 +82,13 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
 		return nil, ErrCantReadAddressFamilyAndProtocol
 	}
 	header.TransportProtocol = AddressFamilyAndProtocol(b14)
-	if _, ok := supportedTransportProtocol[header.TransportProtocol]; !ok {
-		return nil, ErrUnsupportedAddressFamilyAndProtocol
+	//  UNSPEC is only supported when LOCAL is set.
+	if header.TransportProtocol == UNSPEC {
+		if header.Command != LOCAL {
+			return nil, ErrUnsupportedAddressFamilyAndProtocol
+		}
+		// Ignore everything else.
+		return header, nil
 	}
 
 	// Make sure there are bytes available as specified in length
@@ -152,48 +157,54 @@ func (header *Header) formatVersion2() ([]byte, error) {
 	buf.Write(SIGV2)
 	buf.WriteByte(header.Command.toByte())
 	buf.WriteByte(header.TransportProtocol.toByte())
-	var addrSrc, addrDst []byte
-	if header.TransportProtocol.IsIPv4() {
-		hdrLen, err := addTLVLen(lengthV4Bytes, len(header.rawTLVs))
-		if err != nil {
-			return nil, err
+	// When UNSPEC, the receiver must ignore addresses and ports.
+	// Therefore there's no point in writing it.
+	if !header.TransportProtocol.IsUnspec() {
+		var addrSrc, addrDst []byte
+		if header.TransportProtocol.IsIPv4() {
+			hdrLen, err := addTLVLen(lengthV4Bytes, len(header.rawTLVs))
+			if err != nil {
+				return nil, err
+			}
+			buf.Write(hdrLen)
+			sourceIP, destIP, _ := header.IPs()
+			addrSrc = sourceIP.To4()
+			addrDst = destIP.To4()
+		} else if header.TransportProtocol.IsIPv6() {
+			hdrLen, err := addTLVLen(lengthV6Bytes, len(header.rawTLVs))
+			if err != nil {
+				return nil, err
+			}
+			buf.Write(hdrLen)
+			sourceIP, destIP, _ := header.IPs()
+			addrSrc = sourceIP.To16()
+			addrDst = destIP.To16()
+		} else if header.TransportProtocol.IsUnix() {
+			buf.Write(lengthUnixBytes)
+			sourceAddr, destAddr, ok := header.UnixAddrs()
+			if !ok {
+				return nil, ErrInvalidAddress
+			}
+			addrSrc = formatUnixName(sourceAddr.Name)
+			addrDst = formatUnixName(destAddr.Name)
 		}
-		buf.Write(hdrLen)
-		sourceIP, destIP, _ := header.IPs()
-		addrSrc = sourceIP.To4()
-		addrDst = destIP.To4()
-	} else if header.TransportProtocol.IsIPv6() {
-		hdrLen, err := addTLVLen(lengthV6Bytes, len(header.rawTLVs))
-		if err != nil {
-			return nil, err
-		}
-		buf.Write(hdrLen)
-		sourceIP, destIP, _ := header.IPs()
-		addrSrc = sourceIP.To16()
-		addrDst = destIP.To16()
-	} else if header.TransportProtocol.IsUnix() {
-		buf.Write(lengthUnixBytes)
-		sourceAddr, destAddr, ok := header.UnixAddrs()
-		if !ok {
+
+		//
+		if addrSrc == nil || addrDst == nil {
 			return nil, ErrInvalidAddress
 		}
-		addrSrc = formatUnixName(sourceAddr.Name)
-		addrDst = formatUnixName(destAddr.Name)
-	}
-	if addrSrc == nil || addrDst == nil {
-		return nil, ErrInvalidAddress
-	}
-	buf.Write(addrSrc)
-	buf.Write(addrDst)
+		buf.Write(addrSrc)
+		buf.Write(addrDst)
 
-	if sourcePort, destPort, ok := header.Ports(); ok {
-		portBytes := make([]byte, 2)
+		if sourcePort, destPort, ok := header.Ports(); ok {
+			portBytes := make([]byte, 2)
 
-		binary.BigEndian.PutUint16(portBytes, uint16(sourcePort))
-		buf.Write(portBytes)
+			binary.BigEndian.PutUint16(portBytes, uint16(sourcePort))
+			buf.Write(portBytes)
 
-		binary.BigEndian.PutUint16(portBytes, uint16(destPort))
-		buf.Write(portBytes)
+			binary.BigEndian.PutUint16(portBytes, uint16(destPort))
+			buf.Write(portBytes)
+		}
 	}
 
 	if len(header.rawTLVs) > 0 {


Test patch:
diff --git a/v1_test.go b/v1_test.go
index 9b72a79..76628b7 100644
--- a/v1_test.go
+++ b/v1_test.go
@@ -9,63 +9,88 @@ import (
 )
 
 var (
-	TCP4AddressesAndPorts        = strings.Join([]string{IP4_ADDR, IP4_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
-	TCP4AddressesAndInvalidPorts = strings.Join([]string{IP4_ADDR, IP4_ADDR, strconv.Itoa(INVALID_PORT), strconv.Itoa(INVALID_PORT)}, separator)
-	TCP6AddressesAndPorts        = strings.Join([]string{IP6_ADDR, IP6_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
+	IPv4AddressesAndPorts        = strings.Join([]string{IP4_ADDR, IP4_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
+	IPv4AddressesAndInvalidPorts = strings.Join([]string{IP4_ADDR, IP4_ADDR, strconv.Itoa(INVALID_PORT), strconv.Itoa(INVALID_PORT)}, separator)
+	IPv6AddressesAndPorts        = strings.Join([]string{IP6_ADDR, IP6_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
 
-	fixtureTCP4V1 = "PROXY TCP4 " + TCP4AddressesAndPorts + crlf + "GET /"
-	fixtureTCP6V1 = "PROXY TCP6 " + TCP6AddressesAndPorts + crlf + "GET /"
+	fixtureTCP4V1 = "PROXY TCP4 " + IPv4AddressesAndPorts + crlf + "GET /"
+	fixtureTCP6V1 = "PROXY TCP6 " + IPv6AddressesAndPorts + crlf + "GET /"
+
+	fixtureUnknown              = "PROXY UNKNOWN" + crlf
+	fixtureUnknownWithAddresses = "PROXY UNKNOWN " + IPv4AddressesAndInvalidPorts + crlf
 )
 
 var invalidParseV1Tests = []struct {
+	desc          string
 	reader        *bufio.Reader
 	expectedError error
 }{
 	{
-		newBufioReader([]byte("PROX")),
-		ErrNoProxyProtocol,
+		desc:          "no signature",
+		reader:        newBufioReader([]byte(NO_PROTOCOL)),
+		expectedError: ErrNoProxyProtocol,
+	},
+	{
+		desc:          "prox",
+		reader:        newBufioReader([]byte("PROX")),
+		expectedError: ErrNoProxyProtocol,
+	},
+	{
+		desc:          "proxy lf",
+		reader:        newBufioReader([]byte("PROXY \n")),
+		expectedError: ErrLineMustEndWithCrlf,
 	},
 	{
-		newBufioReader([]byte(NO_PROTOCOL)),
-		ErrNoProxyProtocol,
+		desc:          "proxy crlf",
+		reader:        newBufioReader([]byte("PROXY " + crlf)),
+		expectedError: ErrCantReadAddressFamilyAndProtocol,
 	},
 	{
-		newBufioReader([]byte("PROXY \r\n")),
-		ErrCantReadProtocolVersionAndCommand,
+		desc:          "proxy something crlf",
+		reader:        newBufioReader([]byte("PROXY SOMETHING" + crlf)),
+		expectedError: ErrCantReadAddressFamilyAndProtocol,
 	},
 	{
-		newBufioReader([]byte("PROXY TCP4 " + TCP4AddressesAndPorts)),
-		ErrCantReadProtocolVersionAndCommand,
+		desc:          "incomplete signature TCP4",
+		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv4AddressesAndPorts)),
+		expectedError: ErrLineMustEndWithCrlf,
 	},
 	{
-		newBufioReader([]byte("PROXY TCP6 " + TCP4AddressesAndPorts + crlf)),
-		ErrInvalidAddress,
+		desc:          "TCP6 with IPv4 addresses",
+		reader:        newBufioReader([]byte("PROXY TCP6 " + IPv4AddressesAndPorts + crlf)),
+		expectedError: ErrInvalidAddress,
 	},
 	{
-		newBufioReader([]byte("PROXY TCP4 " + TCP6AddressesAndPorts + crlf)),
-		ErrInvalidAddress,
+		desc:          "TCP4 with IPv6 addresses",
+		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv6AddressesAndPorts + crlf)),
+		expectedError: ErrInvalidAddress,
 	},
-	// PROXY TCP IPv4
-	{newBufioReader([]byte("PROXY TCP4 " + TCP4AddressesAndInvalidPorts + crlf)),
-		ErrInvalidPortNumber,
+	{
+		desc:          "TCP4 with invalid port",
+		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv4AddressesAndInvalidPorts + crlf)),
+		expectedError: ErrInvalidPortNumber,
 	},
 }
 
 func TestReadV1Invalid(t *testing.T) {
 	for _, tt := range invalidParseV1Tests {
-		if _, err := Read(tt.reader); err != tt.expectedError {
-			t.Fatalf("TestReadV1Invalid: expected %s, actual %s", tt.expectedError, err.Error())
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			if _, err := Read(tt.reader); err != tt.expectedError {
+				t.Fatalf("expected %s, actual %s", tt.expectedError, err.Error())
+			}
+		})
 	}
 }
 
 var validParseAndWriteV1Tests = []struct {
+	desc           string
 	reader         *bufio.Reader
 	expectedHeader *Header
 }{
 	{
-		bufio.NewReader(strings.NewReader(fixtureTCP4V1)),
-		&Header{
+		desc:   "TCP4",
+		reader: bufio.NewReader(strings.NewReader(fixtureTCP4V1)),
+		expectedHeader: &Header{
 			Version:           1,
 			Command:           PROXY,
 			TransportProtocol: TCPv4,
@@ -74,8 +99,9 @@ var validParseAndWriteV1Tests = []struct {
 		},
 	},
 	{
-		bufio.NewReader(strings.NewReader(fixtureTCP6V1)),
-		&Header{
+		desc:   "TCP6",
+		reader: bufio.NewReader(strings.NewReader(fixtureTCP6V1)),
+		expectedHeader: &Header{
 			Version:           1,
 			Command:           PROXY,
 			TransportProtocol: TCPv6,
@@ -83,38 +109,64 @@ var validParseAndWriteV1Tests = []struct {
 			DestinationAddr:   v6addr,
 		},
 	},
+	{
+		desc:   "unknown",
+		reader: bufio.NewReader(strings.NewReader(fixtureUnknown)),
+		expectedHeader: &Header{
+			Version:           1,
+			Command:           PROXY,
+			TransportProtocol: UNSPEC,
+			SourceAddr:        nil,
+			DestinationAddr:   nil,
+		},
+	},
+	{
+		desc:   "unknown with addresses and ports",
+		reader: bufio.NewReader(strings.NewReader(fixtureUnknownWithAddresses)),
+		expectedHeader: &Header{
+			Version:           1,
+			Command:           PROXY,
+			TransportProtocol: UNSPEC,
+			SourceAddr:        nil,
+			DestinationAddr:   nil,
+		},
+	},
 }
 
 func TestParseV1Valid(t *testing.T) {
 	for _, tt := range validParseAndWriteV1Tests {
-		header, err := Read(tt.reader)
-		if err != nil {
-			t.Fatal("TestParseV1Valid: unexpected error", err.Error())
-		}
-		if !header.EqualsTo(tt.expectedHeader) {
-			t.Fatalf("TestParseV1Valid: expected %#v, actual %#v", tt.expectedHeader, header)
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			header, err := Read(tt.reader)
+			if err != nil {
+				t.Fatal("unexpected error", err.Error())
+			}
+			if !header.EqualsTo(tt.expectedHeader) {
+				t.Fatalf("expected %#v, actual %#v", tt.expectedHeader, header)
+			}
+		})
 	}
 }
 
 func TestWriteV1Valid(t *testing.T) {
 	for _, tt := range validParseAndWriteV1Tests {
-		var b bytes.Buffer
-		w := bufio.NewWriter(&b)
-		if _, err := tt.expectedHeader.WriteTo(w); err != nil {
-			t.Fatal("TestWriteV1Valid: Unexpected error ", err)
-		}
-		w.Flush()
+		t.Run(tt.desc, func(t *testing.T) {
+			var b bytes.Buffer
+			w := bufio.NewWriter(&b)
+			if _, err := tt.expectedHeader.WriteTo(w); err != nil {
+				t.Fatal("unexpected error ", err)
+			}
+			w.Flush()
 
-		// Read written bytes to validate written header
-		r := bufio.NewReader(&b)
-		newHeader, err := Read(r)
-		if err != nil {
-			t.Fatal("TestWriteV1Valid: Unexpected error ", err)
-		}
+			// Read written bytes to validate written header
+			r := bufio.NewReader(&b)
+			newHeader, err := Read(r)
+			if err != nil {
+				t.Fatal("unexpected error ", err)
+			}
 
-		if !newHeader.EqualsTo(tt.expectedHeader) {
-			t.Fatalf("TestWriteV1Valid: expected %#v, actual %#v", tt.expectedHeader, newHeader)
-		}
+			if !newHeader.EqualsTo(tt.expectedHeader) {
+				t.Fatalf("expected %#v, actual %#v", tt.expectedHeader, newHeader)
+			}
+		})
 	}
 }
diff --git a/v2_test.go b/v2_test.go
index 9aa2c99..c6acda1 100644
--- a/v2_test.go
+++ b/v2_test.go
@@ -67,75 +67,96 @@ func pad(b []byte, n int) []byte {
 }
 
 var invalidParseV2Tests = []struct {
+	desc          string
 	reader        *bufio.Reader
 	expectedError error
 }{
 	{
-		newBufioReader(SIGV2[2:]),
-		ErrNoProxyProtocol,
+		desc:          "no signature",
+		reader:        newBufioReader([]byte(NO_PROTOCOL)),
+		expectedError: ErrNoProxyProtocol,
 	},
 	{
-		newBufioReader([]byte(NO_PROTOCOL)),
-		ErrNoProxyProtocol,
+		desc:          "truncated v2 signature",
+		reader:        newBufioReader(SIGV2[2:]),
+		expectedError: ErrNoProxyProtocol,
 	},
 	{
-		newBufioReader(SIGV2),
-		ErrCantReadProtocolVersionAndCommand,
+		desc:          "v2 signature and nothing else",
+		reader:        newBufioReader(SIGV2),
+		expectedError: ErrCantReadProtocolVersionAndCommand,
 	},
 	{
-		newBufioReader(append(SIGV2, invalidRune)),
-		ErrUnsupportedProtocolVersionAndCommand,
+		desc:          "v2 signature with invalid command",
+		reader:        newBufioReader(append(SIGV2, invalidRune)),
+		expectedError: ErrUnsupportedProtocolVersionAndCommand,
 	},
 	{
-		newBufioReader(append(SIGV2, byte(PROXY))),
-		ErrCantReadAddressFamilyAndProtocol,
+		desc:          "v2 signature with command but nothing else",
+		reader:        newBufioReader(append(SIGV2, byte(PROXY))),
+		expectedError: ErrCantReadAddressFamilyAndProtocol,
 	},
 	{
-		newBufioReader(append(SIGV2, byte(PROXY), invalidRune)),
-		ErrUnsupportedAddressFamilyAndProtocol,
+		desc:          "command proxy but inet family unspec",
+		reader:        newBufioReader(append(SIGV2, byte(PROXY), byte(UNSPEC))),
+		expectedError: ErrUnsupportedAddressFamilyAndProtocol,
 	},
 	{
-		newBufioReader(append(SIGV2, byte(PROXY), byte(TCPv4))),
-		ErrCantReadLength,
+		desc:          "v2 signature with command and invalid inet family", // translated to UNSPEC
+		reader:        newBufioReader(append(SIGV2, byte(PROXY), invalidRune)),
+		expectedError: ErrCantReadLength,
 	},
 	{
-		newBufioReader(append(SIGV2, byte(PROXY), byte(TCPv4), invalidRune)),
-		ErrCantReadLength,
+		desc:          "TCPv4 but no length",
+		reader:        newBufioReader(append(SIGV2, byte(PROXY), byte(TCPv4))),
+		expectedError: ErrCantReadLength,
 	},
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), lengthV4Bytes...)),
-		ErrInvalidLength,
+		desc:          "TCPv4 but invalid length",
+		reader:        newBufioReader(append(SIGV2, byte(PROXY), byte(TCPv4), invalidRune)),
+		expectedError: ErrCantReadLength,
 	},
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), lengthV6Bytes...)),
-		ErrInvalidLength,
+		desc:          "TCPv4 with mismatching length",
+		reader:        newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), lengthV4Bytes...)),
+		expectedError: ErrInvalidLength,
 	},
 	{
-		newBufioReader(append(append(append(SIGV2, byte(PROXY), byte(TCPv4)), lengthEmptyBytes...), fixtureIPv6Address...)),
-		ErrInvalidLength,
+		desc:          "TCPv6 with mismatching length",
+		reader:        newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), lengthV6Bytes...)),
+		expectedError: ErrInvalidLength,
 	},
 	{
-		newBufioReader(append(append(append(SIGV2, byte(PROXY), byte(TCPv6)), lengthV6Bytes...), fixtureIPv4Address...)),
-		ErrInvalidLength,
+		desc:          "TCPv4 length zero but with address and ports",
+		reader:        newBufioReader(append(append(append(SIGV2, byte(PROXY), byte(TCPv4)), lengthEmptyBytes...), fixtureIPv6Address...)),
+		expectedError: ErrInvalidLength,
+	},
+	{
+		desc:          "TCPv4 length zero but with address and ports",
+		reader:        newBufioReader(append(append(append(SIGV2, byte(PROXY), byte(TCPv6)), lengthV6Bytes...), fixtureIPv4Address...)),
+		expectedError: ErrInvalidLength,
 	},
 }
 
 func TestParseV2Invalid(t *testing.T) {
 	for _, tt := range invalidParseV2Tests {
-		if _, err := Read(tt.reader); err != tt.expectedError {
-			t.Fatalf("TestParseV2Invalid: expected %s, actual %s", tt.expectedError, err.Error())
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			if _, err := Read(tt.reader); err != tt.expectedError {
+				t.Fatalf("expected %s, actual %s", tt.expectedError, err.Error())
+			}
+		})
 	}
 }
 
 var validParseAndWriteV2Tests = []struct {
+	desc           string
 	reader         *bufio.Reader
 	expectedHeader *Header
 }{
-	// LOCAL
 	{
-		newBufioReader(append(append(SIGV2, byte(LOCAL), byte(TCPv4)), fixtureIPv4V2...)),
-		&Header{
+		desc:   "local",
+		reader: newBufioReader(append(append(SIGV2, byte(LOCAL), byte(TCPv4)), fixtureIPv4V2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           LOCAL,
 			TransportProtocol: TCPv4,
@@ -143,10 +164,21 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   v4addr,
 		},
 	},
-	// PROXY TCP IPv4
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2...)),
-		&Header{
+		desc:   "local unspec",
+		reader: newBufioReader(append(append(SIGV2, byte(LOCAL), byte(TCPv4)), fixtureIPv4V2...)),
+		expectedHeader: &Header{
+			Version:           2,
+			Command:           LOCAL,
+			TransportProtocol: UNSPEC,
+			SourceAddr:        nil,
+			DestinationAddr:   nil,
+		},
+	},
+	{
+		desc:   "proxy TCPv4",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv4,
@@ -154,10 +186,10 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   v4addr,
 		},
 	},
-	// PROXY TCP IPv6
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2...)),
-		&Header{
+		desc:   "proxy TCPv6",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv6,
@@ -165,10 +197,10 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   v6addr,
 		},
 	},
-	// PROXY TCP IPv4 with TLV
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2TLV...)),
-		&Header{
+		desc:   "proxy TCPv4 with TLV",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2TLV...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv4,
@@ -177,10 +209,10 @@ var validParseAndWriteV2Tests = []struct {
 			rawTLVs:           fixtureTLV,
 		},
 	},
-	// PROXY TCP IPv6 with TLV
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2TLV...)),
-		&Header{
+		desc:   "proxy TCPv6 with TLV",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2TLV...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv6,
@@ -189,10 +221,10 @@ var validParseAndWriteV2Tests = []struct {
 			rawTLVs:           fixtureTLV,
 		},
 	},
-	// PROXY UDP IPv4
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(UDPv4)), fixtureIPv4V2...)),
-		&Header{
+		desc:   "proxy UDPv4",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(UDPv4)), fixtureIPv4V2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UDPv4,
@@ -200,10 +232,10 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   v4UDPAddr,
 		},
 	},
-	// PROXY UDP IPv6
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(UDPv6)), fixtureIPv6V2...)),
-		&Header{
+		desc:   "proxy UDPv6",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(UDPv6)), fixtureIPv6V2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UDPv6,
@@ -211,10 +243,10 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   v6UDPAddr,
 		},
 	},
-	// PROXY UNIX STREAM
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(UnixStream)), fixtureUnixV2...)),
-		&Header{
+		desc:   "proxy unix stream",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(UnixStream)), fixtureUnixV2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UnixStream,
@@ -222,10 +254,10 @@ var validParseAndWriteV2Tests = []struct {
 			DestinationAddr:   unixStreamAddr,
 		},
 	},
-	// PROXY UNIX DATAGRAM
 	{
-		newBufioReader(append(append(SIGV2, byte(PROXY), byte(UnixDatagram)), fixtureUnixV2...)),
-		&Header{
+		desc:   "proxy unix datagram ",
+		reader: newBufioReader(append(append(SIGV2, byte(PROXY), byte(UnixDatagram)), fixtureUnixV2...)),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UnixDatagram,
@@ -237,46 +269,51 @@ var validParseAndWriteV2Tests = []struct {
 
 func TestParseV2Valid(t *testing.T) {
 	for _, tt := range validParseAndWriteV2Tests {
-		header, err := Read(tt.reader)
-		if err != nil {
-			t.Fatal("TestParseV2Valid: unexpected error", err.Error())
-		}
-		if !header.EqualsTo(tt.expectedHeader) {
-			t.Fatalf("TestParseV2Valid: expected %#v, actual %#v", tt.expectedHeader, header)
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			header, err := Read(tt.reader)
+			if err != nil {
+				t.Fatal("unexpected error", err.Error())
+			}
+			if !header.EqualsTo(tt.expectedHeader) {
+				t.Fatalf("expected %#v, actual %#v", tt.expectedHeader, header)
+			}
+		})
 	}
 }
 
 func TestWriteV2Valid(t *testing.T) {
 	for _, tt := range validParseAndWriteV2Tests {
-		var b bytes.Buffer
-		w := bufio.NewWriter(&b)
-		if _, err := tt.expectedHeader.WriteTo(w); err != nil {
-			t.Fatal("TestWriteVersion2: Unexpected error ", err)
-		}
-		w.Flush()
+		t.Run(tt.desc, func(t *testing.T) {
+			var b bytes.Buffer
+			w := bufio.NewWriter(&b)
+			if _, err := tt.expectedHeader.WriteTo(w); err != nil {
+				t.Fatal("unexpected error ", err)
+			}
+			w.Flush()
 
-		// Read written bytes to validate written header
-		r := bufio.NewReader(&b)
-		newHeader, err := Read(r)
-		if err != nil {
-			t.Fatal("TestWriteVersion2: Unexpected error ", err)
-		}
+			// Read written bytes to validate written header
+			r := bufio.NewReader(&b)
+			newHeader, err := Read(r)
+			if err != nil {
+				t.Fatal("unexpected error ", err)
+			}
 
-		if !newHeader.EqualsTo(tt.expectedHeader) {
-			t.Fatalf("TestWriteVersion2: expected %#v, actual %#v", tt.expectedHeader, newHeader)
-		}
+			if !newHeader.EqualsTo(tt.expectedHeader) {
+				t.Fatalf("expected %#v, actual %#v", tt.expectedHeader, newHeader)
+			}
+		})
 	}
 }
 
 var validParseV2PaddedTests = []struct {
+	desc           string
 	value          []byte
 	expectedHeader *Header
 }{
-	// PROXY TCP IPv4
 	{
-		append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2Padded...),
-		&Header{
+		desc:  "proxy TCPv4",
+		value: append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2Padded...),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv4,
@@ -285,10 +322,10 @@ var validParseV2PaddedTests = []struct {
 			rawTLVs:           make([]byte, lengthPadded-lengthV4),
 		},
 	},
-	// PROXY TCP IPv6
 	{
-		append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2Padded...),
-		&Header{
+		desc:  "proxy TCPv6",
+		value: append(append(SIGV2, byte(PROXY), byte(TCPv6)), fixtureIPv6V2Padded...),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: TCPv6,
@@ -297,10 +334,10 @@ var validParseV2PaddedTests = []struct {
 			rawTLVs:           make([]byte, lengthPadded-lengthV6),
 		},
 	},
-	// PROXY UDP IPv4
 	{
-		append(append(SIGV2, byte(PROXY), byte(UDPv4)), fixtureIPv4V2Padded...),
-		&Header{
+		desc:  "proxy UDPv4",
+		value: append(append(SIGV2, byte(PROXY), byte(UDPv4)), fixtureIPv4V2Padded...),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UDPv4,
@@ -309,10 +346,10 @@ var validParseV2PaddedTests = []struct {
 			rawTLVs:           make([]byte, lengthPadded-lengthV4),
 		},
 	},
-	// PROXY UDP IPv6
 	{
-		append(append(SIGV2, byte(PROXY), byte(UDPv6)), fixtureIPv6V2Padded...),
-		&Header{
+		desc:  "proxy UDPv6",
+		value: append(append(SIGV2, byte(PROXY), byte(UDPv6)), fixtureIPv6V2Padded...),
+		expectedHeader: &Header{
 			Version:           2,
 			Command:           PROXY,
 			TransportProtocol: UDPv6,
@@ -325,24 +362,26 @@ var validParseV2PaddedTests = []struct {
 
 func TestParseV2Padded(t *testing.T) {
 	for _, tt := range validParseV2PaddedTests {
-		reader := newBufioReader(append(tt.value, arbitraryTailBytes...))
+		t.Run(tt.desc, func(t *testing.T) {
+			reader := newBufioReader(append(tt.value, arbitraryTailBytes...))
 
-		newHeader, err := Read(reader)
-		if err != nil {
-			t.Fatal("TestParseV2Padded: Unexpected error ", err)
-		}
-		if !newHeader.EqualsTo(tt.expectedHeader) {
-			t.Fatalf("TestParseV2Padded: expected %#v, actual %#v", tt.expectedHeader, newHeader)
-		}
+			newHeader, err := Read(reader)
+			if err != nil {
+				t.Fatal("unexpected error ", err)
+			}
+			if !newHeader.EqualsTo(tt.expectedHeader) {
+				t.Fatalf("expected %#v, actual %#v", tt.expectedHeader, newHeader)
+			}
 
-		// Check that remaining padding bytes have been flushed
-		nextBytes, err := reader.Peek(len(arbitraryTailBytes))
-		if err != nil {
-			t.Fatal("TestParseV2Padded: Unexpected error ", err)
-		}
-		if !reflect.DeepEqual(nextBytes, arbitraryTailBytes) {
-			t.Fatalf("TestParseV2Padded: expected %#v, actual %#v", arbitraryTailBytes, nextBytes)
-		}
+			// Check that remaining padding bytes have been flushed
+			nextBytes, err := reader.Peek(len(arbitraryTailBytes))
+			if err != nil {
+				t.Fatal("unexpected error ", err)
+			}
+			if !reflect.DeepEqual(nextBytes, arbitraryTailBytes) {
+				t.Fatalf("expected %#v, actual %#v", arbitraryTailBytes, nextBytes)
+			}
+		})
 	}
 }
 
@@ -356,67 +395,81 @@ func TestV2EqualsToTLV(t *testing.T) {
 	}
 	hdr, err := Read(newBufioReader(append(append(SIGV2, byte(PROXY), byte(TCPv4)), fixtureIPv4V2TLV...)))
 	if err != nil {
-		t.Fatal("TestV2EqualsToTLV: Unexpected error ", err)
+		t.Fatal("unexpected error ", err)
 	}
 	if eHdr.EqualsTo(hdr) {
-		t.Fatalf("TestV2EqualsToTLV: Unexpectedly equal created: %#v, parsed: %#v", eHdr, hdr)
+		t.Fatalf("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)
+		t.Fatalf("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)
+		t.Fatalf("unexpectedly equal after changing tlv created: %#v, parsed: %#v", eHdr, hdr)
 	}
 }
 
-var tlvFormatTests = []*Header{
-	// PROXY TCP IPv4
-	&Header{
-		Version:           2,
-		Command:           PROXY,
-		TransportProtocol: TCPv4,
-		SourceAddr:        v4addr,
-		DestinationAddr:   v4addr,
-		rawTLVs:           make([]byte, 1<<16),
+var tlvFormatTests = []struct {
+	desc   string
+	header *Header
+}{
+	{
+		desc: "proxy TCPv4",
+		header: &Header{
+			Version:           2,
+			Command:           PROXY,
+			TransportProtocol: TCPv4,
+			SourceAddr:        v4addr,
+			DestinationAddr:   v4addr,
+			rawTLVs:           make([]byte, 1<<16),
+		},
 	},
-	// PROXY TCP IPv6
-	&Header{
-		Version:           2,
-		Command:           PROXY,
-		TransportProtocol: TCPv6,
-		SourceAddr:        v6addr,
-		DestinationAddr:   v6addr,
-		rawTLVs:           make([]byte, 1<<16),
+	{
+		desc: "proxy TCPv6",
+		header: &Header{
+			Version:           2,
+			Command:           PROXY,
+			TransportProtocol: TCPv6,
+			SourceAddr:        v6addr,
+			DestinationAddr:   v6addr,
+			rawTLVs:           make([]byte, 1<<16),
+		},
 	},
-	// PROXY UDP IPv4
-	&Header{
-		Version:           2,
-		Command:           PROXY,
-		TransportProtocol: UDPv4,
-		SourceAddr:        v4addr,
-		DestinationAddr:   v4addr,
-		rawTLVs:           make([]byte, 1<<16),
+	{
+		desc: "proxy UDPv4",
+		header: &Header{
+			Version:           2,
+			Command:           PROXY,
+			TransportProtocol: UDPv4,
+			SourceAddr:        v4addr,
+			DestinationAddr:   v4addr,
+			rawTLVs:           make([]byte, 1<<16),
+		},
 	},
-	// PROXY UDP IPv6
-	&Header{
-		Version:           2,
-		Command:           PROXY,
-		TransportProtocol: UDPv6,
-		SourceAddr:        v6addr,
-		DestinationAddr:   v6addr,
-		rawTLVs:           make([]byte, 1<<16),
+	{
+		desc: "proxy UDPv6",
+		header: &Header{
+			Version:           2,
+			Command:           PROXY,
+			TransportProtocol: UDPv6,
+			SourceAddr:        v6addr,
+			DestinationAddr:   v6addr,
+			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)
-		}
+		t.Run(tt.desc, func(t *testing.T) {
+			if _, err := tt.header.Format(); err != errUint16Overflow {
+				t.Fatalf("missing or expected error when formatting too-large TLV %#v", err)
+			}
+		})
+
 	}
 }
 
@@ -429,5 +482,6 @@ func fixtureWithTLV(cur []byte, addr []byte, tlv []byte) []byte {
 	if err != nil {
 		panic(err)
 	}
+
 	return append(append(tlen, addr...), tlv...)
 }