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