New Upstream Release - golang-github-pires-go-proxyproto

Ready changes

Summary

Merged new upstream version: 0.7.0 (was: 0.6.0).

Resulting package

Built on 2023-08-23T08:45 (took 5m19s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-pires-go-proxyproto-dev

Lintian Result

Diff

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..1db23d4
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: pires
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 0000000..5476438
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -0,0 +1,58 @@
+name: golangci-lint
+
+on:
+  push:
+    tags:
+      - v*
+    branches:
+      - main
+  pull_request:
+
+permissions:
+  contents: read
+  # Optional: allow read access to pull request. Use with `only-new-issues` option.
+  # pull-requests: read
+
+jobs:
+  golangci:
+    name: lint
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go: ['1.19', '1.20']
+    steps:
+      - uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.go }}
+      - uses: actions/checkout@v3
+
+      - name: Format
+        run: go fmt
+
+      - name: Vet
+        run: go vet
+
+      - name: lint
+        uses: golangci/golangci-lint-action@v3
+        #with:
+          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
+          #version: v1.29
+
+          # Optional: working directory, useful for monorepos
+          # working-directory: somedir
+
+          # Optional: golangci-lint command line arguments.
+          # args: --issues-exit-code=0
+
+          # Optional: show only new issues if it's a pull request. The default value is `false`.
+          # only-new-issues: true
+
+          # Optional: if set to true then the all caching functionality will be complete disabled,
+          #           takes precedence over all other caching options.
+          # skip-cache: true
+
+          # Optional: if set to true then the action don't cache or restore ~/go/pkg.
+          # skip-pkg-cache: true
+
+          # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
+          # skip-build-cache: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a8a3398
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,16 @@
+name: release
+
+on:
+  push:
+    tags:
+      - "v*.*.*"
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        with:
+          generate_release_notes: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8af161a..80b20dd 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,37 +5,39 @@ on:
   push:
 
 jobs:
-  build:
-    name: Build
+  test:
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
-        go: [ '1.15', '1.14' ]
+        go: ['1.19', '1.20']
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Go
-      uses: actions/setup-go@v2
+    - uses: actions/setup-go@v3
       with:
         go-version: ${{ matrix.go }}
-
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v1
+    - uses: actions/checkout@v3
 
     - name: Get dependencies
       run: |
         go get golang.org/x/tools/cmd/cover
         go get github.com/mattn/goveralls
 
-    - name: Format
-      run: go fmt
-
-    - name: Vet
-      run: go vet
-
     - name: Test
-      run: go test -v -covermode=count -coverprofile=coverage.out
+      run: go test -race -v -covermode=atomic -coverprofile=coverage.out
 
-    - name: actions-goveralls
-      uses: shogo82148/actions-goveralls@v1.2.2
+    - name: Send coverage
+      uses: shogo82148/actions-goveralls@v1
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
+        path-to-profile: coverage.out
+        flag-name: Go-${{ matrix.go }}
+        parallel: true
+
+  # notifies that all test jobs are finished.
+  finish:
+    needs: test
+    runs-on: ubuntu-latest
+    steps:
+      - uses: shogo82148/actions-goveralls@v1
+        with:
+          parallel-finished: true
diff --git a/addr_proto.go b/addr_proto.go
index 687b75d..d254fc4 100644
--- a/addr_proto.go
+++ b/addr_proto.go
@@ -15,32 +15,32 @@ const (
 
 // IsIPv4 returns true if the address family is IPv4 (AF_INET4), false otherwise.
 func (ap AddressFamilyAndProtocol) IsIPv4() bool {
-	return 0x10 == ap&0xF0
+	return ap&0xF0 == 0x10
 }
 
 // IsIPv6 returns true if the address family is IPv6 (AF_INET6), false otherwise.
 func (ap AddressFamilyAndProtocol) IsIPv6() bool {
-	return 0x20 == ap&0xF0
+	return ap&0xF0 == 0x20
 }
 
 // IsUnix returns true if the address family is UNIX (AF_UNIX), false otherwise.
 func (ap AddressFamilyAndProtocol) IsUnix() bool {
-	return 0x30 == ap&0xF0
+	return ap&0xF0 == 0x30
 }
 
 // IsStream returns true if the transport protocol is TCP or STREAM (SOCK_STREAM), false otherwise.
 func (ap AddressFamilyAndProtocol) IsStream() bool {
-	return 0x01 == ap&0x0F
+	return ap&0x0F == 0x01
 }
 
 // IsDatagram returns true if the transport protocol is UDP or DGRAM (SOCK_DGRAM), false otherwise.
 func (ap AddressFamilyAndProtocol) IsDatagram() bool {
-	return 0x02 == ap&0x0F
+	return ap&0x0F == 0x02
 }
 
 // IsUnspec returns true if the transport protocol or address family is unspecified, false otherwise.
 func (ap AddressFamilyAndProtocol) IsUnspec() bool {
-	return (0x00 == ap&0xF0) || (0x00 == ap&0x0F)
+	return (ap&0xF0 == 0x00) || (ap&0x0F == 0x00)
 }
 
 func (ap AddressFamilyAndProtocol) toByte() byte {
diff --git a/debian/changelog b/debian/changelog
index 5f4b130..9a615f3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-pires-go-proxyproto (0.7.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 23 Aug 2023 08:40:41 -0000
+
 golang-github-pires-go-proxyproto (0.6.0-2) unstable; urgency=medium
 
   * Team upload
diff --git a/go.mod b/go.mod
index 0700cb1..82539b3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
 module github.com/pires/go-proxyproto
 
-go 1.13
+go 1.18
diff --git a/header_test.go b/header_test.go
index 9b8662a..1c6d044 100644
--- a/header_test.go
+++ b/header_test.go
@@ -15,6 +15,7 @@ import (
 const (
 	NO_PROTOCOL   = "There is no spoon"
 	IP4_ADDR      = "127.0.0.1"
+	IP4IN6_ADDR   = "::ffff:127.0.0.1"
 	IP6_ADDR      = "::1"
 	IP6_LONG_ADDR = "1234:5678:9abc:def0:cafe:babe:dead:2bad"
 	PORT          = 65533
diff --git a/policy.go b/policy.go
index 71ad62b..6d505be 100644
--- a/policy.go
+++ b/policy.go
@@ -32,8 +32,31 @@ const (
 	// a PROXY header is not present, subsequent reads do not. It is the task
 	// of the code using the connection to handle that case properly.
 	REQUIRE
+	// SKIP accepts a connection without requiring the PROXY header
+	// Note: an example usage can be found in the SkipProxyHeaderForCIDR
+	// function.
+	SKIP
 )
 
+// SkipProxyHeaderForCIDR returns a PolicyFunc which can be used to accept a
+// connection from a skipHeaderCIDR without requiring a PROXY header, e.g.
+// Kubernetes pods local traffic. The def is a policy to use when an upstream
+// address doesn't match the skipHeaderCIDR.
+func SkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) PolicyFunc {
+	return func(upstream net.Addr) (Policy, error) {
+		ip, err := ipFromAddr(upstream)
+		if err != nil {
+			return def, err
+		}
+
+		if skipHeaderCIDR != nil && skipHeaderCIDR.Contains(ip) {
+			return SKIP, nil
+		}
+
+		return def, nil
+	}
+}
+
 // WithPolicy adds given policy to a connection when passed as option to NewConn()
 func WithPolicy(p Policy) func(*Conn) {
 	return func(c *Conn) {
diff --git a/policy_test.go b/policy_test.go
index 40a9444..c8b2624 100644
--- a/policy_test.go
+++ b/policy_test.go
@@ -188,3 +188,26 @@ func Test_MustStrictWhiteListPolicyPanicsWithInvalidIpRange(t *testing.T) {
 
 	MustStrictWhiteListPolicy([]string{"20/80"})
 }
+
+func TestSkipProxyHeaderForCIDR(t *testing.T) {
+	_, cidr, _ := net.ParseCIDR("192.0.2.1/24")
+	f := SkipProxyHeaderForCIDR(cidr, REJECT)
+
+	upstream, _ := net.ResolveTCPAddr("tcp", "192.0.2.255:12345")
+	policy, err := f(upstream)
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	if policy != SKIP {
+		t.Errorf("Expected a SKIP policy for the %s address", upstream)
+	}
+
+	upstream, _ = net.ResolveTCPAddr("tcp", "8.8.8.8:12345")
+	policy, err = f(upstream)
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	if policy != REJECT {
+		t.Errorf("Expected a REJECT policy for the %s address", upstream)
+	}
+}
diff --git a/protocol.go b/protocol.go
index 0f493ba..4ce16a2 100644
--- a/protocol.go
+++ b/protocol.go
@@ -5,13 +5,23 @@ import (
 	"io"
 	"net"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
+// DefaultReadHeaderTimeout is how long header processing waits for header to
+// be read from the wire, if Listener.ReaderHeaderTimeout is not set.
+// It's kept as a global variable so to make it easier to find and override,
+// e.g. go build -ldflags -X "github.com/pires/go-proxyproto.DefaultReadHeaderTimeout=1s"
+var DefaultReadHeaderTimeout = 10 * time.Second
+
 // Listener is used to wrap an underlying listener,
 // whose connections may be using the HAProxy Proxy Protocol.
 // If the connection is using the protocol, the RemoteAddr() will return
-// the correct client address.
+// the correct client address. ReadHeaderTimeout will be applied to all
+// connections in order to prevent blocking operations. If no ReadHeaderTimeout
+// is set, a default of 200ms will be used. This can be disabled by setting the
+// timeout to < 0.
 type Listener struct {
 	Listener          net.Listener
 	Policy            PolicyFunc
@@ -21,15 +31,18 @@ type Listener struct {
 
 // Conn is used to wrap and underlying connection which
 // may be speaking the Proxy Protocol. If it is, the RemoteAddr() will
-// return the address of the client instead of the proxy address.
+// return the address of the client instead of the proxy address. Each connection
+// will have its own readHeaderTimeout and readDeadline set by the Accept() call.
 type Conn struct {
-	bufReader         *bufio.Reader
+	readDeadline      atomic.Value // time.Time
+	once              sync.Once
+	readErr           error
 	conn              net.Conn
+	Validate          Validator
+	bufReader         *bufio.Reader
 	header            *Header
-	once              sync.Once
 	ProxyHeaderPolicy Policy
-	Validate          Validator
-	readErr           error
+	readHeaderTimeout time.Duration
 }
 
 // Validator receives a header and decides whether it is a valid one
@@ -53,10 +66,6 @@ func (p *Listener) Accept() (net.Conn, error) {
 		return nil, err
 	}
 
-	if d := p.ReadHeaderTimeout; d != 0 {
-		conn.SetReadDeadline(time.Now().Add(d))
-	}
-
 	proxyHeaderPolicy := USE
 	if p.Policy != nil {
 		proxyHeaderPolicy, err = p.Policy(conn.RemoteAddr())
@@ -65,6 +74,10 @@ func (p *Listener) Accept() (net.Conn, error) {
 			conn.Close()
 			return nil, err
 		}
+		// Handle a connection as a regular one
+		if proxyHeaderPolicy == SKIP {
+			return conn, nil
+		}
 	}
 
 	newConn := NewConn(
@@ -72,6 +85,15 @@ func (p *Listener) Accept() (net.Conn, error) {
 		WithPolicy(proxyHeaderPolicy),
 		ValidateHeader(p.ValidateHeader),
 	)
+
+	// If the ReadHeaderTimeout for the listener is unset, use the default timeout.
+	if p.ReadHeaderTimeout == 0 {
+		p.ReadHeaderTimeout = DefaultReadHeaderTimeout
+	}
+
+	// Set the readHeaderTimeout of the new conn to the value of the listener
+	newConn.readHeaderTimeout = p.ReadHeaderTimeout
+
 	return newConn, nil
 }
 
@@ -110,6 +132,7 @@ func (p *Conn) Read(b []byte) (int, error) {
 	if p.readErr != nil {
 		return 0, p.readErr
 	}
+
 	return p.bufReader.Read(b)
 }
 
@@ -197,11 +220,16 @@ func (p *Conn) UDPConn() (conn *net.UDPConn, ok bool) {
 
 // SetDeadline wraps original conn.SetDeadline
 func (p *Conn) SetDeadline(t time.Time) error {
+	p.readDeadline.Store(t)
 	return p.conn.SetDeadline(t)
 }
 
 // SetReadDeadline wraps original conn.SetReadDeadline
 func (p *Conn) SetReadDeadline(t time.Time) error {
+	// Set a local var that tells us the desired deadline. This is
+	// needed in order to reset the read deadline to the one that is
+	// desired by the user, rather than an empty deadline.
+	p.readDeadline.Store(t)
 	return p.conn.SetReadDeadline(t)
 }
 
@@ -211,7 +239,36 @@ func (p *Conn) SetWriteDeadline(t time.Time) error {
 }
 
 func (p *Conn) readHeader() error {
+	// If the connection's readHeaderTimeout is more than 0,
+	// push our deadline back to now plus the timeout. This should only
+	// run on the connection, as we don't want to override the previous
+	// read deadline the user may have used.
+	if p.readHeaderTimeout > 0 {
+		if err := p.conn.SetReadDeadline(time.Now().Add(p.readHeaderTimeout)); err != nil {
+			return err
+		}
+	}
+
 	header, err := Read(p.bufReader)
+
+	// If the connection's readHeaderTimeout is more than 0, undo the change to the
+	// deadline that we made above. Because we retain the readDeadline as part of our
+	// SetReadDeadline override, we know the user's desired deadline so we use that.
+	// Therefore, we check whether the error is a net.Timeout and if it is, we decide
+	// the proxy proto does not exist and set the error accordingly.
+	if p.readHeaderTimeout > 0 {
+		t := p.readDeadline.Load()
+		if t == nil {
+			t = time.Time{}
+		}
+		if err := p.conn.SetReadDeadline(t.(time.Time)); err != nil {
+			return err
+		}
+		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+			err = ErrNoProxyProtocol
+		}
+	}
+
 	// For the purpose of this wrapper shamefully stolen from armon/go-proxyproto
 	// let's act as if there was no error when PROXY protocol is not present.
 	if err == ErrNoProxyProtocol {
diff --git a/protocol_test.go b/protocol_test.go
index 3f06815..b1fb2c8 100644
--- a/protocol_test.go
+++ b/protocol_test.go
@@ -6,9 +6,9 @@ package proxyproto
 
 import (
 	"bytes"
-	"context"
 	"crypto/tls"
 	"crypto/x509"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -25,22 +25,29 @@ func TestPassthrough(t *testing.T) {
 
 	pl := &Listener{Listener: l}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
-		conn.Write([]byte("ping"))
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
 		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
+		if _, err = conn.Read(recv); err != nil {
+			cliResult <- err
+			return
 		}
 		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
+			cliResult <- fmt.Errorf("bad: %v", recv)
+			return
 		}
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -61,9 +68,131 @@ func TestPassthrough(t *testing.T) {
 	if _, err := conn.Write([]byte("pong")); err != nil {
 		t.Fatalf("err: %v", err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
+}
+
+// TestRequiredWithReadHeaderTimeout will iterate through 3 different timeouts to see
+// whether using a REQUIRE policy for a listener would cause an error if the timeout
+// is triggerred without a proxy protocol header being defined.
+func TestRequiredWithReadHeaderTimeout(t *testing.T) {
+	for _, duration := range []int{100, 200, 400} {
+		t.Run(fmt.Sprint(duration), func(t *testing.T) {
+			start := time.Now()
+
+			l, err := net.Listen("tcp", "127.0.0.1:0")
+
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+
+			pl := &Listener{
+				Listener:          l,
+				ReadHeaderTimeout: time.Millisecond * time.Duration(duration),
+				Policy: func(upstream net.Addr) (Policy, error) {
+					return REQUIRE, nil
+				},
+			}
+
+			cliResult := make(chan error)
+			go func() {
+				conn, err := net.Dial("tcp", pl.Addr().String())
+				if err != nil {
+					cliResult <- err
+					return
+				}
+				defer conn.Close()
+
+				close(cliResult)
+			}()
+
+			conn, err := pl.Accept()
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+			defer conn.Close()
+
+			// Read blocks forever if there is no ReadHeaderTimeout and the policy is not REQUIRE
+			recv := make([]byte, 4)
+			_, err = conn.Read(recv)
+
+			if err != nil && !errors.Is(err, ErrNoProxyProtocol) && time.Since(start)-pl.ReadHeaderTimeout > 10*time.Millisecond {
+				t.Fatal("proxy proto should not be found and time should be close to read timeout")
+			}
+			err = <-cliResult
+			if err != nil {
+				t.Fatalf("client error: %v", err)
+			}
+		})
+	}
+}
+
+// TestUseWithReadHeaderTimeout will iterate through 3 different timeouts to see
+// whether using a USE policy for a listener would not cause an error if the timeout
+// is triggerred without a proxy protocol header being defined.
+func TestUseWithReadHeaderTimeout(t *testing.T) {
+	for _, duration := range []int{100, 200, 400} {
+		t.Run(fmt.Sprint(duration), func(t *testing.T) {
+			start := time.Now()
+
+			l, err := net.Listen("tcp", "127.0.0.1:0")
+
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+
+			pl := &Listener{
+				Listener:          l,
+				ReadHeaderTimeout: time.Millisecond * time.Duration(duration),
+				Policy: func(upstream net.Addr) (Policy, error) {
+					return USE, nil
+				},
+			}
+
+			cliResult := make(chan error)
+			go func() {
+				conn, err := net.Dial("tcp", pl.Addr().String())
+				if err != nil {
+					cliResult <- err
+					return
+				}
+				defer conn.Close()
+
+				close(cliResult)
+			}()
+
+			conn, err := pl.Accept()
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+			defer conn.Close()
+
+			// 2 times the ReadHeaderTimeout because the first timeout
+			// should occur (the one set on the listener) and allow for the second to follow up
+			if err := conn.SetDeadline(time.Now().Add(pl.ReadHeaderTimeout * 2)); err != nil {
+				t.Fatalf("err: %v", err)
+			}
+
+			// Read blocks forever if there is no ReadHeaderTimeout
+			recv := make([]byte, 4)
+			_, err = conn.Read(recv)
+
+			if err != nil && !errors.Is(err, ErrNoProxyProtocol) && (time.Since(start)-(pl.ReadHeaderTimeout*2)) > 10*time.Millisecond {
+				t.Fatal("proxy proto should not be found and time should be close to read timeout")
+			}
+			err = <-cliResult
+			if err != nil {
+				t.Fatalf("client error: %v", err)
+			}
+		})
+	}
 }
 
-func TestReadHeaderTimeout(t *testing.T) {
+func TestReadHeaderTimeoutIsReset(t *testing.T) {
+	const timeout = time.Millisecond * 250
+
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
 		t.Fatalf("err: %v", err)
@@ -71,20 +200,55 @@ func TestReadHeaderTimeout(t *testing.T) {
 
 	pl := &Listener{
 		Listener:          l,
-		ReadHeaderTimeout: 1 * time.Millisecond,
+		ReadHeaderTimeout: timeout,
 	}
 
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
+	header := &Header{
+		Version:           2,
+		Command:           PROXY,
+		TransportProtocol: TCPv4,
+		SourceAddr: &net.TCPAddr{
+			IP:   net.ParseIP("10.1.1.1"),
+			Port: 1000,
+		},
+		DestinationAddr: &net.TCPAddr{
+			IP:   net.ParseIP("20.2.2.2"),
+			Port: 2000,
+		},
+	}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
-		<-ctx.Done()
+		// Write out the header!
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		// Sleep here longer than the configured timeout.
+		time.Sleep(timeout * 2)
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+		recv := make([]byte, 4)
+		if _, err := conn.Read(recv); err != nil {
+			cliResult <- err
+			return
+		}
+		if !bytes.Equal(recv, []byte("pong")) {
+			cliResult <- fmt.Errorf("bad: %v", recv)
+			return
+		}
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -93,10 +257,203 @@ func TestReadHeaderTimeout(t *testing.T) {
 	}
 	defer conn.Close()
 
-	// Read blocks forever if there is no ReadHeaderTimeout
+	// Set our deadlines higher than our ReadHeaderTimeout
+	if err := conn.SetReadDeadline(time.Now().Add(timeout * 3)); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	if err := conn.SetWriteDeadline(time.Now().Add(timeout * 3)); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
+	if _, err = conn.Read(recv); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	if !bytes.Equal(recv, []byte("ping")) {
+		t.Fatalf("bad: %v", recv)
+	}
+
+	if _, err := conn.Write([]byte("pong")); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	// Check the remote addr
+	addr := conn.RemoteAddr().(*net.TCPAddr)
+	if addr.IP.String() != "10.1.1.1" {
+		t.Fatalf("bad: %v", addr)
+	}
+	if addr.Port != 1000 {
+		t.Fatalf("bad: %v", addr)
+	}
+
+	h := conn.(*Conn).ProxyHeader()
+	if !h.EqualsTo(header) {
+		t.Errorf("bad: %v", h)
+	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
+}
+
+// TestReadHeaderTimeoutIsEmpty ensures the default is set if it is empty.
+// Because the default is 200ms and we wait longer than that to send a message,
+// we expect the actual address and port to be returned,
+// rather than the ProxyHeader we defined.
+func TestReadHeaderTimeoutIsEmpty(t *testing.T) {
+	DefaultReadHeaderTimeout = 200 * time.Millisecond
+
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	pl := &Listener{
+		Listener: l,
+	}
+
+	header := &Header{
+		Version:           2,
+		Command:           PROXY,
+		TransportProtocol: TCPv4,
+		SourceAddr: &net.TCPAddr{
+			IP:   net.ParseIP("10.1.1.1"),
+			Port: 1000,
+		},
+		DestinationAddr: &net.TCPAddr{
+			IP:   net.ParseIP("20.2.2.2"),
+			Port: 2000,
+		},
+	}
+
+	cliResult := make(chan error)
+	go func() {
+		conn, err := net.Dial("tcp", pl.Addr().String())
+		if err != nil {
+			cliResult <- err
+			return
+		}
+		defer conn.Close()
+
+		// Sleep here longer than the configured timeout.
+		time.Sleep(250 * time.Millisecond)
+
+		// Write out the header!
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
+	}()
+
+	conn, err := pl.Accept()
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	defer conn.Close()
+
+	recv := make([]byte, 4)
+	if _, err = conn.Read(recv); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	// Check the remote addr
+	addr := conn.RemoteAddr().(*net.TCPAddr)
+	if addr.IP.String() == "10.1.1.1" {
+		t.Fatalf("bad: %v", addr)
+	}
+	if addr.Port == 1000 {
+		t.Fatalf("bad: %v", addr)
+	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
+}
+
+// TestReadHeaderTimeoutIsNegative does the same as above except
+// with a negative timeout. Therefore, we expect the right ProxyHeader
+// to be returned.
+func TestReadHeaderTimeoutIsNegative(t *testing.T) {
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
 
+	pl := &Listener{
+		Listener:          l,
+		ReadHeaderTimeout: -1,
+	}
+
+	header := &Header{
+		Version:           2,
+		Command:           PROXY,
+		TransportProtocol: TCPv4,
+		SourceAddr: &net.TCPAddr{
+			IP:   net.ParseIP("10.1.1.1"),
+			Port: 1000,
+		},
+		DestinationAddr: &net.TCPAddr{
+			IP:   net.ParseIP("20.2.2.2"),
+			Port: 2000,
+		},
+	}
+
+	cliResult := make(chan error)
+	go func() {
+		conn, err := net.Dial("tcp", pl.Addr().String())
+		if err != nil {
+			cliResult <- err
+			return
+		}
+		defer conn.Close()
+
+		// Sleep here longer than the configured timeout.
+		time.Sleep(250 * time.Millisecond)
+
+		// Write out the header!
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
+	}()
+
+	conn, err := pl.Accept()
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	defer conn.Close()
+
+	recv := make([]byte, 4)
+	if _, err = conn.Read(recv); err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	// Check the remote addr
+	addr := conn.RemoteAddr().(*net.TCPAddr)
+	if addr.IP.String() != "10.1.1.1" {
+		t.Fatalf("bad: %v", addr)
+	}
+	if addr.Port != 1000 {
+		t.Fatalf("bad: %v", addr)
+	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestParse_ipv4(t *testing.T) {
@@ -120,25 +477,37 @@ func TestParse_ipv4(t *testing.T) {
 			Port: 2000,
 		},
 	}
+
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
 		// Write out the header!
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
 
-		conn.Write([]byte("ping"))
 		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
+		if _, err = conn.Read(recv); err != nil {
+			cliResult <- err
+			return
 		}
 		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
+			cliResult <- fmt.Errorf("bad: %v", recv)
+			return
 		}
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -148,8 +517,7 @@ func TestParse_ipv4(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
+	if _, err = conn.Read(recv); err != nil {
 		t.Fatalf("err: %v", err)
 	}
 	if !bytes.Equal(recv, []byte("ping")) {
@@ -173,6 +541,10 @@ func TestParse_ipv4(t *testing.T) {
 	if !h.EqualsTo(header) {
 		t.Errorf("bad: %v", h)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestParse_ipv6(t *testing.T) {
@@ -197,25 +569,36 @@ func TestParse_ipv6(t *testing.T) {
 		},
 	}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
 		// Write out the header!
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
 
-		conn.Write([]byte("ping"))
 		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
+		if _, err = conn.Read(recv); err != nil {
+			cliResult <- err
+			return
 		}
 		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
+			cliResult <- fmt.Errorf("bad: %v", recv)
+			return
 		}
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -225,8 +608,7 @@ func TestParse_ipv6(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
+	if _, err = conn.Read(recv); err != nil {
 		t.Fatalf("err: %v", err)
 	}
 	if !bytes.Equal(recv, []byte("ping")) {
@@ -250,6 +632,10 @@ func TestParse_ipv6(t *testing.T) {
 	if !h.EqualsTo(header) {
 		t.Errorf("bad: %v", h)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestAcceptReturnsErrorWhenPolicyFuncErrors(t *testing.T) {
@@ -263,12 +649,16 @@ func TestAcceptReturnsErrorWhenPolicyFuncErrors(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -279,6 +669,10 @@ func TestAcceptReturnsErrorWhenPolicyFuncErrors(t *testing.T) {
 	if conn != nil {
 		t.Fatalf("Expected no connection, got %v", conn)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestReadingIsRefusedWhenProxyHeaderRequiredButMissing(t *testing.T) {
@@ -291,13 +685,21 @@ func TestReadingIsRefusedWhenProxyHeaderRequiredButMissing(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
-		conn.Write([]byte("ping"))
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -307,10 +709,13 @@ func TestReadingIsRefusedWhenProxyHeaderRequiredButMissing(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != ErrNoProxyProtocol {
+	if _, err = conn.Read(recv); err != ErrNoProxyProtocol {
 		t.Fatalf("Expected error %v, received %v", ErrNoProxyProtocol, err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestReadingIsRefusedWhenProxyHeaderPresentButNotAllowed(t *testing.T) {
@@ -323,10 +728,12 @@ func TestReadingIsRefusedWhenProxyHeaderPresentButNotAllowed(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 		header := &Header{
@@ -342,7 +749,12 @@ func TestReadingIsRefusedWhenProxyHeaderPresentButNotAllowed(t *testing.T) {
 				Port: 2000,
 			},
 		}
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -352,10 +764,13 @@ func TestReadingIsRefusedWhenProxyHeaderPresentButNotAllowed(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != ErrSuperfluousProxyHeader {
+	if _, err = conn.Read(recv); err != ErrSuperfluousProxyHeader {
 		t.Fatalf("Expected error %v, received %v", ErrSuperfluousProxyHeader, err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 func TestIgnorePolicyIgnoresIpFromProxyHeader(t *testing.T) {
 	l, err := net.Listen("tcp", "127.0.0.1:0")
@@ -367,10 +782,12 @@ func TestIgnorePolicyIgnoresIpFromProxyHeader(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
@@ -388,17 +805,27 @@ func TestIgnorePolicyIgnoresIpFromProxyHeader(t *testing.T) {
 				Port: 2000,
 			},
 		}
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
 
-		conn.Write([]byte("ping"))
 		recv := make([]byte, 4)
-		_, err = conn.Read(recv)
-		if err != nil {
-			t.Fatalf("err: %v", err)
+		if _, err = conn.Read(recv); err != nil {
+			cliResult <- err
+			return
 		}
 		if !bytes.Equal(recv, []byte("pong")) {
-			t.Fatalf("bad: %v", recv)
+			cliResult <- fmt.Errorf("bad: %v", recv)
+			return
 		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -408,8 +835,7 @@ func TestIgnorePolicyIgnoresIpFromProxyHeader(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != nil {
+	if _, err = conn.Read(recv); err != nil {
 		t.Fatalf("err: %v", err)
 	}
 	if !bytes.Equal(recv, []byte("ping")) {
@@ -425,6 +851,10 @@ func TestIgnorePolicyIgnoresIpFromProxyHeader(t *testing.T) {
 	if addr.IP.String() != "127.0.0.1" {
 		t.Fatalf("bad: %v", addr)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func Test_AllOptionsAreRecognized(t *testing.T) {
@@ -465,13 +895,21 @@ func TestReadingIsRefusedOnErrorWhenRemoteAddrRequestedFirst(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
-		conn.Write([]byte("ping"))
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -482,10 +920,13 @@ func TestReadingIsRefusedOnErrorWhenRemoteAddrRequestedFirst(t *testing.T) {
 
 	_ = conn.RemoteAddr()
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != ErrNoProxyProtocol {
+	if _, err = conn.Read(recv); err != ErrNoProxyProtocol {
 		t.Fatalf("Expected error %v, received %v", ErrNoProxyProtocol, err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestReadingIsRefusedOnErrorWhenLocalAddrRequestedFirst(t *testing.T) {
@@ -498,13 +939,21 @@ func TestReadingIsRefusedOnErrorWhenLocalAddrRequestedFirst(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
-		conn.Write([]byte("ping"))
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -515,10 +964,70 @@ func TestReadingIsRefusedOnErrorWhenLocalAddrRequestedFirst(t *testing.T) {
 
 	_ = conn.LocalAddr()
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != ErrNoProxyProtocol {
+	if _, err = conn.Read(recv); err != ErrNoProxyProtocol {
 		t.Fatalf("Expected error %v, received %v", ErrNoProxyProtocol, err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
+}
+
+func TestSkipProxyProtocolPolicy(t *testing.T) {
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+
+	policyFunc := func(upstream net.Addr) (Policy, error) { return SKIP, nil }
+
+	pl := &Listener{
+		Listener: l,
+		Policy:   policyFunc,
+	}
+
+	cliResult := make(chan error)
+	ping := []byte("ping")
+	go func() {
+		conn, err := net.Dial("tcp", pl.Addr().String())
+		if err != nil {
+			cliResult <- err
+			return
+		}
+		defer conn.Close()
+
+		if _, err := conn.Write(ping); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
+	}()
+
+	conn, err := pl.Accept()
+	if err != nil {
+		t.Fatalf("err: %v", err)
+	}
+	defer conn.Close()
+
+	_, ok := conn.(*net.TCPConn)
+	if !ok {
+		t.Fatal("err: should be a tcp connection")
+	}
+	_ = conn.LocalAddr()
+	recv := make([]byte, 4)
+	if _, err = conn.Read(recv); err != nil {
+		t.Fatalf("Unexpected read error: %v", err)
+	}
+
+	if !bytes.Equal(ping, recv) {
+		t.Fatalf("Unexpected %s data while expected %s", recv, ping)
+	}
+
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func Test_ConnectionCasts(t *testing.T) {
@@ -531,13 +1040,21 @@ func Test_ConnectionCasts(t *testing.T) {
 
 	pl := &Listener{Listener: l, Policy: policyFunc}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
-		conn.Write([]byte("ping"))
+
+		if _, err := conn.Write([]byte("ping")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -563,6 +1080,10 @@ func Test_ConnectionCasts(t *testing.T) {
 	if !ok {
 		t.Fatal("err: should be a tcp connection")
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func Test_ConnectionErrorsWhenHeaderValidationFails(t *testing.T) {
@@ -574,10 +1095,12 @@ func Test_ConnectionErrorsWhenHeaderValidationFails(t *testing.T) {
 	validationError := fmt.Errorf("failed to validate")
 	pl := &Listener{Listener: l, ValidateHeader: func(*Header) error { return validationError }}
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := net.Dial("tcp", pl.Addr().String())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
@@ -595,7 +1118,12 @@ func Test_ConnectionErrorsWhenHeaderValidationFails(t *testing.T) {
 				Port: 2000,
 			},
 		}
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := pl.Accept()
@@ -605,10 +1133,13 @@ func Test_ConnectionErrorsWhenHeaderValidationFails(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 4)
-	_, err = conn.Read(recv)
-	if err != validationError {
+	if _, err = conn.Read(recv); err != validationError {
 		t.Fatalf("expected validation error, got %v", err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 type TestTLSServer struct {
@@ -673,10 +1204,12 @@ func Test_TLSServer(t *testing.T) {
 	}
 	defer s.Close()
 
+	cliResult := make(chan error)
 	go func() {
 		conn, err := tls.Dial("tcp", s.Addr(), s.TLSClientConfig)
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
@@ -694,9 +1227,17 @@ func Test_TLSServer(t *testing.T) {
 				Port: 2000,
 			},
 		}
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
 
-		conn.Write([]byte("test"))
+		if _, err := conn.Write([]byte("test")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := s.Listener.Accept()
@@ -713,6 +1254,10 @@ func Test_TLSServer(t *testing.T) {
 	if string(recv[:n]) != "test" {
 		t.Fatalf("expected \"test\", got \"%s\" %v", recv[:n], recv[:n])
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func Test_MisconfiguredTLSServerRespondsWithUnderlyingError(t *testing.T) {
@@ -730,6 +1275,7 @@ func Test_MisconfiguredTLSServerRespondsWithUnderlyingError(t *testing.T) {
 	}
 	defer s.Close()
 
+	cliResult := make(chan error)
 	go func() {
 		// this is not a valid TLS connection, we are
 		// connecting to the TLS endpoint via plain TCP.
@@ -745,7 +1291,8 @@ func Test_MisconfiguredTLSServerRespondsWithUnderlyingError(t *testing.T) {
 
 		conn, err := net.Dial("tcp", s.Addr())
 		if err != nil {
-			t.Fatalf("err: %v", err)
+			cliResult <- err
+			return
 		}
 		defer conn.Close()
 
@@ -763,9 +1310,17 @@ func Test_MisconfiguredTLSServerRespondsWithUnderlyingError(t *testing.T) {
 				Port: 2000,
 			},
 		}
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			cliResult <- err
+			return
+		}
 
-		conn.Write([]byte("GET /foo/bar HTTP/1.1"))
+		if _, err := conn.Write([]byte("GET /foo/bar HTTP/1.1")); err != nil {
+			cliResult <- err
+			return
+		}
+
+		close(cliResult)
 	}()
 
 	conn, err := s.Listener.Accept()
@@ -775,10 +1330,13 @@ func Test_MisconfiguredTLSServerRespondsWithUnderlyingError(t *testing.T) {
 	defer conn.Close()
 
 	recv := make([]byte, 1024)
-	_, err = conn.Read(recv)
-	if err.Error() != "tls: first record does not look like a TLS handshake" {
+	if _, err = conn.Read(recv); err.Error() != "tls: first record does not look like a TLS handshake" {
 		t.Fatalf("expected tls handshake error, got %s", err)
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 type testConn struct {
@@ -808,7 +1366,9 @@ func TestCopyToWrappedConnection(t *testing.T) {
 	wrappedConn := NewConn(innerConn)
 	dummySrc := &testConn{reads: 1}
 
-	io.Copy(wrappedConn, dummySrc)
+	if _, err := io.Copy(wrappedConn, dummySrc); err != nil {
+		t.Fatalf("err: %v", err)
+	}
 	if innerConn.readFromCalledWith != dummySrc {
 		t.Error("Expected io.Copy to delegate to ReadFrom function of inner destination connection")
 	}
@@ -818,7 +1378,9 @@ func TestCopyFromWrappedConnection(t *testing.T) {
 	wrappedConn := NewConn(&testConn{reads: 1})
 	dummyDst := &testConn{}
 
-	io.Copy(dummyDst, wrappedConn)
+	if _, err := io.Copy(dummyDst, wrappedConn); err != nil {
+		t.Fatalf("err: %v", err)
+	}
 	if dummyDst.readFromCalledWith != wrappedConn.conn {
 		t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom method of destination")
 	}
@@ -830,7 +1392,9 @@ func TestCopyFromWrappedConnectionToWrappedConnection(t *testing.T) {
 	innerConn2 := &testConn{}
 	wrappedConn2 := NewConn(innerConn2)
 
-	io.Copy(wrappedConn1, wrappedConn2)
+	if _, err := io.Copy(wrappedConn1, wrappedConn2); err != nil {
+		t.Fatalf("err: %v", err)
+	}
 	if innerConn1.readFromCalledWith != innerConn2 {
 		t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom of inner destination connection")
 	}
@@ -850,9 +1414,10 @@ func benchmarkTCPProxy(size int, b *testing.B) {
 				break
 			}
 			_, err = io.Copy(conn, conn)
-			conn.Close()
+			// Can't defer since we keep accepting on each for iteration.
+			_ = conn.Close()
 			if err != nil {
-				b.Fatalf("Failed to read entire payload: %v", err)
+				panic(fmt.Sprintf("Failed to read entire payload: %v", err))
 			}
 		}
 	}()
@@ -872,21 +1437,21 @@ func benchmarkTCPProxy(size int, b *testing.B) {
 			}
 			bConn, err := net.Dial("tcp", backend.Addr().String())
 			if err != nil {
-				b.Fatalf("failed to dial backend: %v", err)
+				panic(fmt.Sprintf("failed to dial backend: %v", err))
 			}
 			go func() {
 				_, err = io.Copy(bConn, conn)
+				_ = bConn.(*net.TCPConn).CloseWrite()
 				if err != nil {
-					b.Fatalf("Failed to proxy incoming data to backend: %v", err)
+					panic(fmt.Sprintf("Failed to proxy incoming data to backend: %v", err))
 				}
-				bConn.(*net.TCPConn).CloseWrite()
 			}()
 			_, err = io.Copy(conn, bConn)
 			if err != nil {
-				b.Fatalf("Failed to proxy data from backend: %v", err)
+				panic(fmt.Sprintf("Failed to proxy data from backend: %v", err))
 			}
-			conn.Close()
-			bConn.Close()
+			_ = conn.Close()
+			_ = bConn.Close()
 		}
 	}()
 
@@ -914,14 +1479,16 @@ func benchmarkTCPProxy(size int, b *testing.B) {
 			b.Fatalf("err: %v", err)
 		}
 		// Write out the header!
-		header.WriteTo(conn)
+		if _, err := header.WriteTo(conn); err != nil {
+			b.Fatalf("err: %v", err)
+		}
 		//send data
 		go func() {
 			_, err = conn.Write(data)
+			_ = conn.(*net.TCPConn).CloseWrite()
 			if err != nil {
-				b.Fatalf("Failed to write data: %v", err)
+				panic(fmt.Sprintf("Failed to write data: %v", err))
 			}
-			conn.(*net.TCPConn).CloseWrite()
 
 		}()
 		//receive data
diff --git a/tlv_test.go b/tlv_test.go
index 1f7285c..6373cb2 100644
--- a/tlv_test.go
+++ b/tlv_test.go
@@ -13,30 +13,6 @@ var (
 	fixturePartialLenTLV = []byte{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
-}
-
 var invalidTLVTests = []struct {
 	name          string
 	reader        *bufio.Reader
diff --git a/tlvparse/gcp.go b/tlvparse/gcp.go
new file mode 100644
index 0000000..2c51184
--- /dev/null
+++ b/tlvparse/gcp.go
@@ -0,0 +1,47 @@
+package tlvparse
+
+import (
+	"encoding/binary"
+
+	"github.com/pires/go-proxyproto"
+)
+
+const (
+	// PP2_TYPE_GCP indicates a Google Cloud Platform header
+	PP2_TYPE_GCP proxyproto.PP2Type = 0xE0
+)
+
+// ExtractPSCConnectionID returns the first PSC Connection ID in the TLV if it exists and is well-formed and
+// a bool indicating one was found.
+func ExtractPSCConnectionID(tlvs []proxyproto.TLV) (uint64, bool) {
+	for _, tlv := range tlvs {
+		if linkID, err := pscConnectionID(tlv); err == nil {
+			return linkID, true
+		}
+	}
+	return 0, false
+}
+
+// pscConnectionID returns the ID of a GCP PSC extension TLV or errors with ErrIncompatibleTLV or
+// ErrMalformedTLV if it's the wrong TLV type or is malformed.
+//
+//	Field	Length (bytes)	Description
+//	Type	1	PP2_TYPE_GCP (0xE0)
+//	Length	2	Length of value (always 0x0008)
+//	Value	8	The 8-byte PSC Connection ID (decode to uint64; big endian)
+//
+// For example proxyproto.TLV{Type:0xea, Length:8, Value:[]byte{0xff, 0xff, 0xff, 0xff, 0xc0, 0xa8, 0x64, 0x02}}
+// will be decoded as 18446744072646845442.
+//
+// See https://cloud.google.com/vpc/docs/configure-private-service-connect-producer
+func pscConnectionID(t proxyproto.TLV) (uint64, error) {
+	if !isPSCConnectionID(t) {
+		return 0, proxyproto.ErrIncompatibleTLV
+	}
+	linkID := binary.BigEndian.Uint64(t.Value)
+	return linkID, nil
+}
+
+func isPSCConnectionID(t proxyproto.TLV) bool {
+	return t.Type == PP2_TYPE_GCP && len(t.Value) == 8
+}
diff --git a/tlvparse/gcp_test.go b/tlvparse/gcp_test.go
new file mode 100644
index 0000000..f3aee41
--- /dev/null
+++ b/tlvparse/gcp_test.go
@@ -0,0 +1,82 @@
+package tlvparse
+
+import (
+	"testing"
+
+	"github.com/pires/go-proxyproto"
+)
+
+func TestExtractPSCConnectionID(t *testing.T) {
+	tests := []struct {
+		name                string
+		tlvs                []proxyproto.TLV
+		wantPSCConnectionID uint64
+		wantFound           bool
+	}{
+		{
+			name:      "nil TLVs",
+			tlvs:      nil,
+			wantFound: false,
+		},
+		{
+			name:      "empty TLVs",
+			tlvs:      []proxyproto.TLV{},
+			wantFound: false,
+		},
+		{
+			name: "AWS VPC endpoint ID",
+			tlvs: []proxyproto.TLV{
+				{
+					Type:  0xEA,
+					Value: []byte{0x01, 0x76, 0x70, 0x63, 0x65, 0x2d, 0x61, 0x62, 0x63, 0x31, 0x32, 0x33},
+				},
+			},
+			wantFound: false,
+		},
+		{
+			name: "GCP link ID",
+			tlvs: []proxyproto.TLV{
+				{
+					Type:  PP2_TYPE_GCP,
+					Value: []byte{'\xff', '\xff', '\xff', '\xff', '\xc0', '\xa8', '\x64', '\x02'},
+				},
+			},
+			wantPSCConnectionID: 18446744072646845442,
+			wantFound:           true,
+		},
+		{
+			name: "Multiple TLVs",
+			tlvs: []proxyproto.TLV{
+				{ // AWS
+					Type:  0xEA,
+					Value: []byte{0x01, 0x76, 0x70, 0x63, 0x65, 0x2d, 0x61, 0x62, 0x63, 0x31, 0x32, 0x33},
+				},
+				{ // Azure
+					Type:  0xEE,
+					Value: []byte{0x02, 0x01, 0x01, 0x01, 0x01},
+				},
+				{ // GCP but wrong length
+					Type:  0xE0,
+					Value: []byte{0xff, 0xff, 0xff},
+				},
+				{ // Correct
+					Type:  0xE0,
+					Value: []byte{'\xff', '\xff', '\xff', '\xff', '\xc0', '\xa8', '\x64', '\x02'},
+				},
+			},
+			wantPSCConnectionID: 18446744072646845442,
+			wantFound:           true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			linkID, hasLinkID := ExtractPSCConnectionID(tt.tlvs)
+			if hasLinkID != tt.wantFound {
+				t.Errorf("ExtractPSCConnectionID() got1 = %v, want %v", hasLinkID, tt.wantFound)
+			}
+			if linkID != tt.wantPSCConnectionID {
+				t.Errorf("ExtractPSCConnectionID() got = %v, want %v", linkID, tt.wantPSCConnectionID)
+			}
+		})
+	}
+}
diff --git a/tlvparse/ssl.go b/tlvparse/ssl.go
index 5f62e2a..7575671 100644
--- a/tlvparse/ssl.go
+++ b/tlvparse/ssl.go
@@ -11,8 +11,8 @@ import (
 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
+	PP2_BITFIELD_CLIENT_CERT_CONN uint8 = 0x02
+	PP2_BITFIELD_CLIENT_CERT_SESS uint8 = 0x04
 
 	tlvSSLMinLen = 5 // len(pp2_tlv_ssl.client) + len(pp2_tlv_ssl.verify)
 )
@@ -65,6 +65,16 @@ func (s PP2SSL) SSLVersion() (string, bool) {
 	return "", false
 }
 
+// SSLCipher returns the US-ASCII string representation of the used TLS cipher and whether that extension exists.
+func (s PP2SSL) SSLCipher() (string, bool) {
+	for _, tlv := range s.TLV {
+		if tlv.Type == proxyproto.PP2_SUBTYPE_SSL_CIPHER {
+			return string(tlv.Value), true
+		}
+	}
+	return "", false
+}
+
 // Marshal formats the PP2SSL structure as a TLV.
 func (s PP2SSL) Marshal() (proxyproto.TLV, error) {
 	v := make([]byte, 5)
@@ -138,6 +148,14 @@ func SSL(t proxyproto.TLV) (PP2SSL, error) {
 			if len(tlv.Value) == 0 || !utf8.Valid(tlv.Value) {
 				return PP2SSL{}, proxyproto.ErrMalformedTLV
 			}
+		case proxyproto.PP2_SUBTYPE_SSL_CIPHER:
+			/*
+				The second level TLV PP2_SUBTYPE_SSL_CIPHER provides the US-ASCII string name
+				of the used cipher, for example "ECDHE-RSA-AES128-GCM-SHA256".
+			*/
+			if len(tlv.Value) == 0 || !isASCII(tlv.Value) {
+				return PP2SSL{}, proxyproto.ErrMalformedTLV
+			}
 		}
 	}
 	if !versionFound {
diff --git a/tlvparse/ssl_test.go b/tlvparse/ssl_test.go
index e98f87b..a198cbe 100644
--- a/tlvparse/ssl_test.go
+++ b/tlvparse/ssl_test.go
@@ -74,11 +74,85 @@ var testCases = []struct {
 				t.Fatalf("TestParseV2TLV %s: Unexpected SSLVersion expected %#v, actual %#v", name, esslVer, asslVer)
 			}
 
+			if _, ok := ssl.SSLCipher(); ok {
+				t.Fatalf("TestParseV2TLV %s: Unexpected SSLCipher", name)
+			}
+
 			if !ssl.Verified() {
 				t.Fatalf("TestParseV2TLV %s: Expected Verified to be true", name)
 			}
 		},
 	},
+	{
+		name: "SSL haproxy cipher",
+		raw: []byte{
+			0x0d, 0x0a, 0x0d, 0x0a,
+			0x00, 0x0d, 0x0a, 0x51,
+			0x55, 0x49, 0x54, 0x0a,
+			0x21, 0x21, 0x00, 0x4f,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0xff, 0xff,
+			0x0a, 0x01, 0x5b, 0x0e,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0xff, 0xff,
+			0x0a, 0x01, 0x01, 0x9f,
+			0xf4, 0x7c, 0x01, 0xbb,
+			0x20, 0x00, 0x28, 0x01,
+			0x00, 0x00, 0x00, 0x00,
+			0x21, 0x00, 0x07, 0x54,
+			0x4c, 0x53, 0x76, 0x31,
+			0x2e, 0x33, 0x23, 0x00,
+			0x16, 0x54, 0x4c, 0x53,
+			0x5f, 0x41, 0x45, 0x53,
+			0x5f, 0x32, 0x35, 0x36,
+			0x5f, 0x47, 0x43, 0x4d,
+			0x5f, 0x53, 0x48, 0x41,
+			0x33, 0x38, 0x34,
+		},
+		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 false", name)
+			}
+
+			if ssl.ClientCertSess() {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientCertSess() to be false", name)
+			}
+
+			if _, ok := ssl.ClientCN(); ok {
+				t.Fatalf("TestParseV2TLV %s: Expected ClientCN to not exist", name)
+			}
+
+			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)
+			}
+
+			esslCipher := "TLS_AES_256_GCM_SHA384"
+			if asslCipher, ok := ssl.SSLCipher(); !ok {
+				t.Fatalf("TestParseV2TLV %s: Expected SSLCipher to exist", name)
+			} else if asslCipher != esslCipher {
+				t.Fatalf("TestParseV2TLV %s: Unexpected SSLCipher expected %#v, actual %#v", name, esslCipher, asslCipher)
+			}
+		},
+	},
 }
 
 func TestParseV2TLV(t *testing.T) {
diff --git a/v1.go b/v1.go
index 23de95e..0d34ba5 100644
--- a/v1.go
+++ b/v1.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"fmt"
 	"net"
+	"net/netip"
 	"strconv"
 	"strings"
 )
@@ -221,11 +222,22 @@ func parseV1PortNumber(portStr string) (int, error) {
 	return port, nil
 }
 
-func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (addr net.IP, err error) {
-	addr = net.ParseIP(addrStr)
-	tryV4 := addr.To4()
-	if (protocol == TCPv4 && tryV4 == nil) || (protocol == TCPv6 && tryV4 != nil) {
-		err = ErrInvalidAddress
+func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP, error) {
+	addr, err := netip.ParseAddr(addrStr)
+	if err != nil {
+		return nil, ErrInvalidAddress
 	}
-	return
+
+	switch protocol {
+	case TCPv4:
+		if addr.Is4() {
+			return net.IP(addr.AsSlice()), nil
+		}
+	case TCPv6:
+		if addr.Is6() || addr.Is4In6() {
+			return net.IP(addr.AsSlice()), nil
+		}
+	}
+
+	return nil, ErrInvalidAddress
 }
diff --git a/v1_test.go b/v1_test.go
index 0267580..616dc15 100644
--- a/v1_test.go
+++ b/v1_test.go
@@ -3,6 +3,8 @@ package proxyproto
 import (
 	"bufio"
 	"bytes"
+	"errors"
+	"fmt"
 	"io"
 	"net"
 	"strconv"
@@ -13,12 +15,14 @@ import (
 
 var (
 	IPv4AddressesAndPorts        = strings.Join([]string{IP4_ADDR, IP4_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
+	IPv4In6AddressesAndPorts     = strings.Join([]string{IP4IN6_ADDR, IP4IN6_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)
 	IPv6LongAddressesAndPorts    = strings.Join([]string{IP6_LONG_ADDR, IP6_LONG_ADDR, strconv.Itoa(PORT), strconv.Itoa(PORT)}, separator)
 
-	fixtureTCP4V1 = "PROXY TCP4 " + IPv4AddressesAndPorts + crlf + "GET /"
-	fixtureTCP6V1 = "PROXY TCP6 " + IPv6AddressesAndPorts + crlf + "GET /"
+	fixtureTCP4V1    = "PROXY TCP4 " + IPv4AddressesAndPorts + crlf + "GET /"
+	fixtureTCP6V1    = "PROXY TCP6 " + IPv6AddressesAndPorts + crlf + "GET /"
+	fixtureTCP4IN6V1 = "PROXY TCP6 " + IPv4In6AddressesAndPorts + crlf + "GET /"
 
 	fixtureTCP6V1Overflow = "PROXY TCP6 " + IPv6LongAddressesAndPorts
 
@@ -66,6 +70,11 @@ var invalidParseV1Tests = []struct {
 		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv4AddressesAndPorts)),
 		expectedError: ErrCantReadVersion1Header,
 	},
+	{
+		desc:          "invalid IP address",
+		reader:        newBufioReader([]byte("PROXY TCP4 invalid invalid 65533 65533" + crlf)),
+		expectedError: ErrInvalidAddress,
+	},
 	{
 		desc:          "TCP6 with IPv4 addresses",
 		reader:        newBufioReader([]byte("PROXY TCP6 " + IPv4AddressesAndPorts + crlf)),
@@ -76,6 +85,11 @@ var invalidParseV1Tests = []struct {
 		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv6AddressesAndPorts + crlf)),
 		expectedError: ErrInvalidAddress,
 	},
+	{
+		desc:          "TCP4 with IPv4 mapped addresses",
+		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv4In6AddressesAndPorts + crlf)),
+		expectedError: ErrInvalidAddress,
+	},
 	{
 		desc:          "TCP4 with invalid port",
 		reader:        newBufioReader([]byte("PROXY TCP4 " + IPv4AddressesAndInvalidPorts + crlf)),
@@ -102,6 +116,7 @@ var validParseAndWriteV1Tests = []struct {
 	desc           string
 	reader         *bufio.Reader
 	expectedHeader *Header
+	skipWrite      bool
 }{
 	{
 		desc:   "TCP4",
@@ -125,6 +140,21 @@ var validParseAndWriteV1Tests = []struct {
 			DestinationAddr:   v6addr,
 		},
 	},
+	{
+		desc:   "TCP4IN6",
+		reader: bufio.NewReader(strings.NewReader(fixtureTCP4IN6V1)),
+		expectedHeader: &Header{
+			Version:           1,
+			Command:           PROXY,
+			TransportProtocol: TCPv6,
+			SourceAddr:        v4addr,
+			DestinationAddr:   v4addr,
+		},
+		// we skip write test because net.ParseIP converts ::ffff:127.0.0.1 to v4
+		// instead of preserving the v4 in v6 form, so, after serializing the header,
+		// we end up with v6 protocol and a v4 IP which is invalid
+		skipWrite: true,
+	},
 	{
 		desc:   "unknown",
 		reader: bufio.NewReader(strings.NewReader(fixtureUnknown)),
@@ -165,6 +195,9 @@ func TestParseV1Valid(t *testing.T) {
 
 func TestWriteV1Valid(t *testing.T) {
 	for _, tt := range validParseAndWriteV1Tests {
+		if tt.skipWrite {
+			continue
+		}
 		t.Run(tt.desc, func(t *testing.T) {
 			var b bytes.Buffer
 			w := bufio.NewWriter(&b)
@@ -214,7 +247,7 @@ func TestParseVersion1Overflow(t *testing.T) {
 	reader := bufio.NewReader(ds)
 	bufSize := reader.Size()
 	ds.NBytes = bufSize * 16
-	parseVersion1(reader)
+	_, _ = parseVersion1(reader)
 	if ds.NRead > bufSize {
 		t.Fatalf("read: expected max %d bytes, actual %d\n", bufSize, ds.NRead)
 	}
@@ -228,10 +261,13 @@ func listen(t *testing.T) *Listener {
 	return &Listener{Listener: l}
 }
 
-func client(t *testing.T, addr, header string, length int, terminate bool, wait time.Duration, done chan struct{}) {
+func client(t *testing.T, addr, header string, length int, terminate bool, wait time.Duration, done chan struct{},
+	result chan error,
+) {
 	c, err := net.Dial("tcp", addr)
 	if err != nil {
-		t.Fatalf("dial: %v", err)
+		result <- fmt.Errorf("dial: %w", err)
+		return
 	}
 	defer c.Close()
 
@@ -250,21 +286,25 @@ func client(t *testing.T, addr, header string, length int, terminate bool, wait
 
 	n, err := c.Write(buf)
 	if err != nil {
-		t.Fatalf("write: %v", err)
+		result <- fmt.Errorf("write: %w", err)
+		return
 	}
 	if n != len(buf) {
-		t.Fatalf("write; short write")
+		result <- errors.New("write; short write")
+		return
 	}
 
+	close(result)
 	time.Sleep(wait)
 	close(done)
 }
 
 func TestVersion1Overflow(t *testing.T) {
 	done := make(chan struct{})
+	cliResult := make(chan error)
 
 	l := listen(t)
-	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 10240, true, 10*time.Second, done)
+	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 10240, true, 10*time.Second, done, cliResult)
 
 	c, err := l.Accept()
 	if err != nil {
@@ -276,14 +316,19 @@ func TestVersion1Overflow(t *testing.T) {
 	if err == nil {
 		t.Fatalf("net.Conn: no error reported for oversized header")
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestVersion1SlowLoris(t *testing.T) {
 	done := make(chan struct{})
+	cliResult := make(chan error)
 	timeout := make(chan error)
 
 	l := listen(t)
-	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 0, false, 10*time.Second, done)
+	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 0, false, 10*time.Second, done, cliResult)
 
 	c, err := l.Accept()
 	if err != nil {
@@ -304,14 +349,19 @@ func TestVersion1SlowLoris(t *testing.T) {
 			t.Fatalf("net.Conn: no error reported for incomplete header")
 		}
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }
 
 func TestVersion1SlowLorisOverflow(t *testing.T) {
 	done := make(chan struct{})
+	cliResult := make(chan error)
 	timeout := make(chan error)
 
 	l := listen(t)
-	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 10240, false, 10*time.Second, done)
+	go client(t, l.Addr().String(), fixtureTCP6V1Overflow, 10240, false, 10*time.Second, done, cliResult)
 
 	c, err := l.Accept()
 	if err != nil {
@@ -332,4 +382,8 @@ func TestVersion1SlowLorisOverflow(t *testing.T) {
 			t.Fatalf("net.Conn: no error reported for incomplete and overflowed header")
 		}
 	}
+	err = <-cliResult
+	if err != nil {
+		t.Fatalf("client error: %v", err)
+	}
 }

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/pires/go-proxyproto/tlvparse/gcp.go
-rw-r--r--  root/root   /usr/share/gocode/src/github.com/pires/go-proxyproto/tlvparse/gcp_test.go

No differences were encountered in the control files

More details

Full run details