This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
version: "2"
linters:
default: standard
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- copyloopvar
- goconst
- godot
- gosec
- modernize
- misspell
- revive
- unconvert
- usestdlibvars
settings:
goconst:
# Newer goconst versions default ignore-calls to false, which flags
# idiomatic call-argument strings such as t.Fatalf("err: %v", err) and
# net.Dial("tcp", addr). Restore the prior default so only genuinely
# repeated constant values are reported.
ignore-calls: true
run:
timeout: 5m
allow-parallel-runners: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
+125 -122
View File
@@ -1,162 +1,165 @@
# go-proxyproto
[![Actions Status](https://github.com/pires/go-proxyproto/workflows/test/badge.svg)](https://github.com/pires/go-proxyproto/actions)
[![Coverage Status](https://coveralls.io/repos/github/pires/go-proxyproto/badge.svg?branch=master)](https://coveralls.io/github/pires/go-proxyproto?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/pires/go-proxyproto/badge.svg?branch=main)](https://coveralls.io/github/pires/go-proxyproto?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/pires/go-proxyproto)](https://goreportcard.com/report/github.com/pires/go-proxyproto)
[![](https://godoc.org/github.com/pires/go-proxyproto?status.svg)](https://pkg.go.dev/github.com/pires/go-proxyproto?tab=doc)
[![Go Reference](https://pkg.go.dev/badge/github.com/pires/go-proxyproto.svg)](https://pkg.go.dev/github.com/pires/go-proxyproto)
A Go library implementation of the [PROXY protocol, versions 1 and 2](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt),
which provides, as per specification:
> (...) a convenient way to safely transport connection
> information such as a client's address across multiple layers of NAT or TCP
> proxies. It is designed to require little changes to existing components and
> to limit the performance impact caused by the processing of the transported
> information.
A Go library implementation of the
[HAProxy PROXY protocol specification 3.4](https://www.haproxy.org/download/3.4/doc/proxy-protocol.txt).
It covers protocol versions 1 (text) and 2 (binary), carrying the original
client connection information across NAT and proxy layers.
This library is to be used in one of or both proxy clients and proxy servers that need to support said protocol.
Both protocol versions, 1 (text-based) and 2 (binary-based) are supported.
Use it on either side of a PROXY-aware hop: send headers with `Header.WriteTo`
or `Header.FormatUDPDatagram`, and receive them with `Listener`, `NewConn`,
`Read`, or `ParseUDPDatagram`. The stream APIs are for TCP and Unix streams;
UDP uses packet helpers because the spec requires a header in every datagram.
## Installation
```shell
$ go get -u github.com/pires/go-proxyproto
$ go get github.com/pires/go-proxyproto
```
## Examples
The fastest way to get started is the runnable programs under
[`examples/`](examples) and the API examples on
[pkg.go.dev](https://pkg.go.dev/github.com/pires/go-proxyproto#pkg-examples):
| Goal | Where to look |
| ---- | ------------- |
| Minimal client | [`examples/client`](examples/client) |
| Minimal server | [`examples/server`](examples/server) |
| HTTP server | [`examples/httpserver`](examples/httpserver) |
| Server + client over TLS (PROXY header before TLS) | [`examples/tlsserver`](examples/tlsserver), [`examples/tlsclient`](examples/tlsclient) |
| UDP receiver and sender | [`examples/udpserver`](examples/udpserver), [`examples/udpclient`](examples/udpclient) |
| UDP `net.PacketConn` wrapper pattern | [`examples/udppacketconn`](examples/udppacketconn) |
| `Listener`, `NewConn`, `Read`, UDP, and TLS API examples | [Package examples](https://pkg.go.dev/github.com/pires/go-proxyproto#pkg-examples) |
## Usage
### Client
Use the runnable examples above for complete programs. The core API shape is
small.
### Client side
```go
package main
header := proxyproto.HeaderProxyFromAddrs(1, sourceAddr, destinationAddr)
_, err := header.WriteTo(conn) // write the PROXY header before application data
```
import (
"io"
"log"
"net"
See [`examples/client`](examples/client) for a complete TCP client.
proxyproto "github.com/pires/go-proxyproto"
)
### Server side
func chkErr(err error) {
if err != nil {
log.Fatalf("Error: %s", err.Error())
}
}
```go
proxyListener := &proxyproto.Listener{Listener: ln}
conn, err := proxyListener.Accept()
// Connections must open with a PROXY header (the default policy is REQUIRE);
// conn.RemoteAddr() then reports the client address from that header.
```
func main() {
// Dial some proxy listener e.g. https://github.com/mailgun/proxyproto
target, err := net.ResolveTCPAddr("tcp", "127.0.0.1:2319")
chkErr(err)
See [`examples/server`](examples/server) for a complete TCP server. For HTTP/1
and HTTP/2, see [`examples/httpserver`](examples/httpserver), which uses
[`helper/http2`](helper/http2) so one server can accept proxied HTTP/1 and HTTP/2
connections.
conn, err := net.DialTCP("tcp", nil, target)
chkErr(err)
> [!WARNING]
> The zero-value configuration requires the PROXY header but still honors it
> from any peer. It is not safe for listeners reachable by untrusted clients
> without a trusted-source policy. See [Security](#security).
defer conn.Close()
## Security
// Create a proxyprotocol header or use HeaderProxyFromAddrs() if you
// have two conn's
header := &proxyproto.Header{
Version: 1,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.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,
},
}
// After the connection was created write the proxy headers first
_, err = header.WriteTo(conn)
chkErr(err)
// Then your data... e.g.:
_, err = io.WriteString(conn, "HELO")
chkErr(err)
The PROXY header replaces what your application sees as the client address, so
whoever is allowed to send one can spoof their origin. The spec (section 2)
says receivers "MUST not try to guess" whether the header is present, and
requires access filtering so only trusted proxies can use the protocol.
Important stream defaults and policies:
- With no policy configured, `Listener` and `NewConn` use
`proxyproto.DefaultPolicy`, which is `REQUIRE`. A connection that does not
open with a PROXY header fails its first I/O with `ErrNoProxyProtocol`, so
header presence is never guessed.
- `REQUIRE` still honors headers from any peer. Use it only when the listener
is reachable exclusively by trusted proxies, such as a private network
segment behind your load balancer.
- Deployments that need historical optional-header behavior can restore it
process-wide with `proxyproto.DefaultPolicy = proxyproto.USE`, per
connection with `WithPolicy(USE)`, or per listener with a policy returning
`USE`.
- For exposed listeners, restrict the senders with `TrustProxyHeaderFrom` or
`TrustProxyHeaderFromRanges`. Trusted peers must send a header; untrusted
peers are dropped by `Accept`. For example:
```go
proxyListener := &proxyproto.Listener{
Listener: ln,
// Connections from the load balancer must open with a PROXY header
// (REQUIRE); connections from any other source are dropped. For CIDR
// ranges, use TrustProxyHeaderFromRanges([]string{"10.0.0.0/24"}).
// For mixed traffic (e.g. optional headers from some sources), spell the
// two policies out with PolicyFromRanges(ranges, matched, unmatched).
ConnPolicy: proxyproto.TrustProxyHeaderFrom(net.ParseIP("10.0.0.10")),
}
```
### Server
For UDP, `ParseUDPDatagram` has no built-in trusted-source policy because it
only sees packet bytes. Check the `net.PacketConn.ReadFrom` sender address
yourself before trusting `header.SourceAddr`.
```go
package main
Related knobs are documented in the
[package docs](https://pkg.go.dev/github.com/pires/go-proxyproto):
`ReadHeaderTimeout` (default 10s), `MaxV2HeaderSize` (default 4KiB),
`V1AcceptIPv4InTCP6` (default off), and `Listener.ValidateHeader`.
import (
"log"
"net"
## UDP
proxyproto "github.com/pires/go-proxyproto"
)
The spec requires the header and proxied payload in the same UDP datagram, and
the receiver must parse the header independently for every datagram. `Listener`
and `Conn` cannot provide those semantics; use `ParseUDPDatagram` and
`Header.FormatUDPDatagram` with your own `net.PacketConn`. Headers with
`UDPv4`/`UDPv6` families carried over stream connections describe the proxied
protocol and remain fully supported.
func main() {
// Create a listener
addr := "localhost:9876"
list, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("couldn't listen to %q: %q\n", addr, err.Error())
}
- [`examples/udpserver`](examples/udpserver) and
[`examples/udpclient`](examples/udpclient) show direct per-datagram parsing
and formatting.
- [`examples/udppacketconn`](examples/udppacketconn) sketches a
`net.PacketConn` wrapper with reply routing through the relaying proxy.
- [`ExampleParseUDPDatagram`](https://pkg.go.dev/github.com/pires/go-proxyproto#example-ParseUDPDatagram)
and
[`ExampleHeader_FormatUDPDatagram`](https://pkg.go.dev/github.com/pires/go-proxyproto#example-Header.FormatUDPDatagram)
show the same APIs as package examples.
// Wrap listener in a proxyproto listener
proxyListener := &proxyproto.Listener{Listener: list}
defer proxyListener.Close()
The library deliberately does not export a `net.PacketConn` wrapper. A wrapper
that returns the client address from the header also needs an application-owned
client-to-proxy flow table for replies, including bounds, expiry, and spoofing
policy.
// Wait for a connection and accept it
conn, err := proxyListener.Accept()
defer conn.Close()
### TLS
// Print connection details
if conn.LocalAddr() == nil {
log.Fatal("couldn't retrieve local address")
}
log.Printf("local address: %q", conn.LocalAddr().String())
When combining PROXY protocol with TLS, match the wrapper order to the upstream
order. If the header is sent in cleartext before the handshake, put proxyproto
inside TLS: `tls.NewListener(&proxyproto.Listener{Listener: l}, tlsConfig)`.
If the header is sent inside the TLS session, decrypt first:
`&proxyproto.Listener{Listener: tls.NewListener(l, tlsConfig)}`. In both cases
`conn.RemoteAddr()` reports the client carried by the PROXY header.
if conn.RemoteAddr() == nil {
log.Fatal("couldn't retrieve remote address")
}
log.Printf("remote address: %q", conn.RemoteAddr().String())
}
```
### HTTP Server
```go
package main
import (
"net"
"net/http"
"time"
"github.com/pires/go-proxyproto"
)
func main() {
server := http.Server{
Addr: ":8080",
}
ln, err := net.Listen("tcp", server.Addr)
if err != nil {
panic(err)
}
proxyListener := &proxyproto.Listener{
Listener: ln,
ReadHeaderTimeout: 10 * time.Second,
}
defer proxyListener.Close()
server.Serve(proxyListener)
}
```
Runnable code lives in [`examples/tlsserver`](examples/tlsserver) and
[`examples/tlsclient`](examples/tlsclient). Package examples show both
orderings.
## Special notes
### AWS
AWS Network Load Balancer (NLB) does not push the PPV2 header until the client starts sending the data. This is a problem if your server speaks first. e.g. SMTP, FTP, SSH etc.
By default, NLB target group attribute `proxy_protocol_v2.client_to_server.header_placement` has the value `on_first_ack_with_payload`. You need to contact AWS support to change it to `on_first_ack`, instead.
Just to be clear, you need this fix only if your server is designed to speak first.
AWS Network Load Balancer (NLB) does not send the PROXY v2 header until the
client sends payload: the target group attribute
`proxy_protocol_v2.client_to_server.header_placement` defaults to
`on_first_ack_with_payload`. Server-first protocols such as SMTP, FTP, and SSH
fail in that mode; contact AWS support to change the attribute to
`on_first_ack` so the header arrives before the backend speaks.
+18
View File
@@ -3,6 +3,7 @@ package proxyproto
// AddressFamilyAndProtocol represents address family and transport protocol.
type AddressFamilyAndProtocol byte
// AddressFamilyAndProtocol enum values.
const (
UNSPEC AddressFamilyAndProtocol = '\x00'
TCPv4 AddressFamilyAndProtocol = '\x11'
@@ -13,6 +14,23 @@ const (
UnixDatagram AddressFamilyAndProtocol = '\x32'
)
// supportedTransportProtocol is the set of address-family/transport-protocol
// bytes defined by the PROXY protocol v2 spec (section 2.2). Any other value is
// rejected during v2 parsing. This matters for bytes that share a known family
// but an undefined transport (e.g. 0x13: IPv4 family, transport 3): they pass
// the IsIPv4/IsIPv6 family checks yet are neither stream nor datagram, so
// newIPAddr would return a nil net.Addr and later panic callers of
// RemoteAddr().String() / LocalAddr().String().
var supportedTransportProtocol = map[AddressFamilyAndProtocol]bool{
UNSPEC: true,
TCPv4: true,
UDPv4: true,
TCPv6: true,
UDPv6: true,
UnixStream: true,
UnixDatagram: true,
}
// IsIPv4 returns true if the address family is IPv4 (AF_INET4), false otherwise.
func (ap AddressFamilyAndProtocol) IsIPv4() bool {
return ap&0xF0 == 0x10
+139 -37
View File
@@ -1,5 +1,5 @@
// Package proxyproto implements Proxy Protocol (v1 and v2) parser and writer, as per specification:
// https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt
// https://www.haproxy.org/download/3.4/doc/proxy-protocol.txt
package proxyproto
import (
@@ -11,27 +11,51 @@ import (
"time"
)
// Network names for Unix-domain addresses, matching net.UnixAddr.Net values.
// The net package exposes no constants for these.
const (
networkUnix = "unix"
networkUnixgram = "unixgram"
)
var (
// Protocol
// SIGV1 is the signature for PROXY protocol v1.
SIGV1 = []byte{'\x50', '\x52', '\x4F', '\x58', '\x59'}
// SIGV2 is the signature for PROXY protocol v2.
SIGV2 = []byte{'\x0D', '\x0A', '\x0D', '\x0A', '\x00', '\x0D', '\x0A', '\x51', '\x55', '\x49', '\x54', '\x0A'}
ErrCantReadVersion1Header = errors.New("proxyproto: can't read version 1 header")
ErrVersion1HeaderTooLong = errors.New("proxyproto: version 1 header must be 107 bytes or less")
ErrLineMustEndWithCrlf = errors.New("proxyproto: version 1 header is invalid, must end with \\r\\n")
ErrCantReadProtocolVersionAndCommand = errors.New("proxyproto: can't read proxy protocol version and command")
ErrCantReadAddressFamilyAndProtocol = errors.New("proxyproto: can't read address family or protocol")
ErrCantReadLength = errors.New("proxyproto: can't read length")
ErrCantResolveSourceUnixAddress = errors.New("proxyproto: can't resolve source Unix address")
ErrCantResolveDestinationUnixAddress = errors.New("proxyproto: can't resolve destination Unix address")
ErrNoProxyProtocol = errors.New("proxyproto: proxy protocol signature not present")
ErrUnknownProxyProtocolVersion = errors.New("proxyproto: unknown proxy protocol version")
// ErrCantReadVersion1Header indicates a v1 header could not be read.
ErrCantReadVersion1Header = errors.New("proxyproto: can't read version 1 header")
// ErrVersion1HeaderTooLong indicates a v1 header is too long.
ErrVersion1HeaderTooLong = errors.New("proxyproto: version 1 header must be 107 bytes or less")
// ErrLineMustEndWithCrlf indicates a v1 header is invalid, must end with \r\n.
ErrLineMustEndWithCrlf = errors.New("proxyproto: version 1 header is invalid, must end with \\r\\n")
// ErrCantReadProtocolVersionAndCommand indicates a protocol version and command could not be read.
ErrCantReadProtocolVersionAndCommand = errors.New("proxyproto: can't read proxy protocol version and command")
// ErrCantReadAddressFamilyAndProtocol indicates an address family and protocol could not be read.
ErrCantReadAddressFamilyAndProtocol = errors.New("proxyproto: can't read address family or protocol")
// ErrCantReadLength indicates a length could not be read.
ErrCantReadLength = errors.New("proxyproto: can't read length")
// ErrCantResolveSourceUnixAddress indicates a source Unix address could not be resolved.
ErrCantResolveSourceUnixAddress = errors.New("proxyproto: can't resolve source Unix address")
// ErrCantResolveDestinationUnixAddress indicates a destination Unix address could not be resolved.
ErrCantResolveDestinationUnixAddress = errors.New("proxyproto: can't resolve destination Unix address")
// ErrNoProxyProtocol indicates a proxy protocol signature is not present.
ErrNoProxyProtocol = errors.New("proxyproto: proxy protocol signature not present")
// ErrUnknownProxyProtocolVersion indicates an unknown proxy protocol version.
ErrUnknownProxyProtocolVersion = errors.New("proxyproto: unknown proxy protocol version")
// ErrUnsupportedProtocolVersionAndCommand indicates an unsupported protocol version and command.
ErrUnsupportedProtocolVersionAndCommand = errors.New("proxyproto: unsupported proxy protocol version and command")
ErrUnsupportedAddressFamilyAndProtocol = errors.New("proxyproto: unsupported address family and protocol")
ErrInvalidLength = errors.New("proxyproto: invalid length")
ErrInvalidAddress = errors.New("proxyproto: invalid address")
ErrInvalidPortNumber = errors.New("proxyproto: invalid port number")
ErrSuperfluousProxyHeader = errors.New("proxyproto: upstream connection sent PROXY header but isn't allowed to send one")
// ErrUnsupportedAddressFamilyAndProtocol indicates an unsupported address family and protocol.
ErrUnsupportedAddressFamilyAndProtocol = errors.New("proxyproto: unsupported address family and protocol")
// ErrInvalidLength indicates an invalid length.
ErrInvalidLength = errors.New("proxyproto: invalid length")
// ErrInvalidAddress indicates an invalid address.
ErrInvalidAddress = errors.New("proxyproto: invalid address")
// ErrInvalidPortNumber indicates an invalid port number.
ErrInvalidPortNumber = errors.New("proxyproto: invalid port number")
// ErrSuperfluousProxyHeader indicates an upstream connection sent a PROXY header but isn't allowed to send one.
ErrSuperfluousProxyHeader = errors.New("proxyproto: upstream connection sent PROXY header but isn't allowed to send one")
)
// Header is the placeholder for proxy protocol header.
@@ -61,31 +85,50 @@ func HeaderProxyFromAddrs(version byte, sourceAddr, destAddr net.Addr) *Header {
}
switch sourceAddr := sourceAddr.(type) {
case *net.TCPAddr:
if _, ok := destAddr.(*net.TCPAddr); !ok {
// Both ends must be the same Addr type; bind destAddr to read its IP below.
destAddr, ok := destAddr.(*net.TCPAddr)
if !ok {
break
}
if len(sourceAddr.IP.To4()) == net.IPv4len {
// Pick the family from BOTH addresses, not just the source: use v4 only
// when both are IPv4, otherwise fall back to v6 (the v4 side is then
// serialized as a v4-mapped IPv6, ::ffff:x.x.x.x). The previous
// source-only check mislabeled a v4-source/v6-dest pair as TCPv4 and then
// failed in formatVersion1.
switch {
case sourceAddr.IP.To4() != nil && destAddr.IP.To4() != nil:
h.TransportProtocol = TCPv4
} else if len(sourceAddr.IP) == net.IPv6len {
case sourceAddr.IP.To16() != nil && destAddr.IP.To16() != nil:
h.TransportProtocol = TCPv6
}
case *net.UDPAddr:
if _, ok := destAddr.(*net.UDPAddr); !ok {
destAddr, ok := destAddr.(*net.UDPAddr)
if !ok {
break
}
if len(sourceAddr.IP.To4()) == net.IPv4len {
// Same both-ends family selection as TCP above.
switch {
case sourceAddr.IP.To4() != nil && destAddr.IP.To4() != nil:
h.TransportProtocol = UDPv4
} else if len(sourceAddr.IP) == net.IPv6len {
case sourceAddr.IP.To16() != nil && destAddr.IP.To16() != nil:
h.TransportProtocol = UDPv6
}
case *net.UnixAddr:
if _, ok := destAddr.(*net.UnixAddr); !ok {
destAddr, ok := destAddr.(*net.UnixAddr)
if !ok {
break
}
// Both ends must agree on stream vs datagram: there is no meaningful
// connection mixing the two, so a mismatched pair stays UNSPEC rather than
// being labeled with the source's flavor alone. Mirrors the both-ends
// family selection used for TCP/UDP above.
if sourceAddr.Net != destAddr.Net {
break
}
switch sourceAddr.Net {
case "unix":
case networkUnix:
h.TransportProtocol = UnixStream
case "unixgram":
case networkUnixgram:
h.TransportProtocol = UnixDatagram
}
}
@@ -97,6 +140,7 @@ func HeaderProxyFromAddrs(version byte, sourceAddr, destAddr net.Addr) *Header {
return h
}
// TCPAddrs returns TCP source/destination addresses if the header is stream-based.
func (header *Header) TCPAddrs() (sourceAddr, destAddr *net.TCPAddr, ok bool) {
if !header.TransportProtocol.IsStream() {
return nil, nil, false
@@ -106,6 +150,7 @@ func (header *Header) TCPAddrs() (sourceAddr, destAddr *net.TCPAddr, ok bool) {
return sourceAddr, destAddr, sourceOK && destOK
}
// UDPAddrs returns UDP source/destination addresses if the header is datagram-based.
func (header *Header) UDPAddrs() (sourceAddr, destAddr *net.UDPAddr, ok bool) {
if !header.TransportProtocol.IsDatagram() {
return nil, nil, false
@@ -115,6 +160,7 @@ func (header *Header) UDPAddrs() (sourceAddr, destAddr *net.UDPAddr, ok bool) {
return sourceAddr, destAddr, sourceOK && destOK
}
// UnixAddrs returns UNIX source/destination addresses if the header is UNIX-based.
func (header *Header) UnixAddrs() (sourceAddr, destAddr *net.UnixAddr, ok bool) {
if !header.TransportProtocol.IsUnix() {
return nil, nil, false
@@ -124,24 +170,26 @@ func (header *Header) UnixAddrs() (sourceAddr, destAddr *net.UnixAddr, ok bool)
return sourceAddr, destAddr, sourceOK && destOK
}
// IPs returns source/destination IPs for TCP/UDP headers.
func (header *Header) IPs() (sourceIP, destIP net.IP, ok bool) {
if sourceAddr, destAddr, ok := header.TCPAddrs(); ok {
return sourceAddr.IP, destAddr.IP, true
} else if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
return sourceAddr.IP, destAddr.IP, true
} else {
return nil, nil, false
}
if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
return sourceAddr.IP, destAddr.IP, true
}
return nil, nil, false
}
// Ports returns source/destination ports for TCP/UDP headers.
func (header *Header) Ports() (sourcePort, destPort int, ok bool) {
if sourceAddr, destAddr, ok := header.TCPAddrs(); ok {
return sourceAddr.Port, destAddr.Port, true
} else if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
return sourceAddr.Port, destAddr.Port, true
} else {
return 0, 0, false
}
if sourceAddr, destAddr, ok := header.UDPAddrs(); ok {
return sourceAddr.Port, destAddr.Port, true
}
return 0, 0, false
}
// EqualTo returns true if headers are equivalent, false otherwise.
@@ -166,8 +214,17 @@ func (header *Header) EqualsTo(otherHeader *Header) bool {
if header.Command == LOCAL {
return true
}
return header.SourceAddr.String() == otherHeader.SourceAddr.String() &&
header.DestinationAddr.String() == otherHeader.DestinationAddr.String()
return addrsEqual(header.SourceAddr, otherHeader.SourceAddr) &&
addrsEqual(header.DestinationAddr, otherHeader.DestinationAddr)
}
// addrsEqual compares two net.Addr by their rendered form, without panicking
// when either side is nil (hand-built PROXY headers may lack addresses).
func addrsEqual(a, b net.Addr) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
return a.String() == b.String()
}
// WriteTo renders a proxy protocol header in a format and writes it to an io.Writer.
@@ -177,7 +234,7 @@ func (header *Header) WriteTo(w io.Writer) (int64, error) {
return 0, err
}
return bytes.NewBuffer(buf).WriteTo(w)
return bytes.NewReader(buf).WriteTo(w)
}
// Format renders a proxy protocol header in a format to write over the wire.
@@ -256,6 +313,17 @@ func Read(reader *bufio.Reader) (*Header, error) {
// ReadTimeout acts as Read but takes a timeout. If that timeout is reached, it's assumed
// there's no proxy protocol header.
//
// Deprecated: ReadTimeout cannot cancel the read it starts. It only receives a
// *bufio.Reader, so on timeout it has no way to set a deadline on or close the
// underlying connection: the goroutine it spawns stays blocked in Read, peeking
// at the stalled connection, until the peer sends data or the connection is
// closed elsewhere. Each timed-out call therefore leaks that goroutine and the
// connection's file descriptor. Use ReadHeaderTimeout instead, which also takes
// the net.Conn and sets a real read deadline so the read is actually cancelled
// on timeout; or wrap the connection with NewConn or a Listener and configure
// the header timeout via the SetReadHeaderTimeout option or
// Listener.ReadHeaderTimeout.
func ReadTimeout(reader *bufio.Reader, timeout time.Duration) (*Header, error) {
type header struct {
h *Header
@@ -278,3 +346,37 @@ func ReadTimeout(reader *bufio.Reader, timeout time.Duration) (*Header, error) {
return nil, ErrNoProxyProtocol
}
}
// ReadHeaderTimeout reads the PROXY protocol header from conn, giving up after
// timeout. It is the cancellable replacement for the deprecated ReadTimeout:
// because it is given the net.Conn, it sets a read deadline so a stalled read is
// actually interrupted instead of leaking a blocked goroutine and the
// connection's file descriptor. If the timeout is reached it returns
// ErrNoProxyProtocol, assuming no header is present.
//
// reader must be buffered over conn (for example bufio.NewReader(conn)); it is
// used for the header read so that any bytes buffered past the header remain
// available for the caller to read afterwards. A timeout <= 0 reads without a
// deadline.
//
// ReadHeaderTimeout overwrites conn's read deadline and restores the zero (no)
// deadline before returning; re-apply your own read deadline afterwards if you
// had one set.
func ReadHeaderTimeout(conn net.Conn, reader *bufio.Reader, timeout time.Duration) (*Header, error) {
if timeout > 0 {
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return nil, err
}
// Best-effort restore of the zero deadline. A failure here (e.g. the
// peer has already closed) must not mask a header we parsed, so the
// error is intentionally ignored; the header/err from Read is
// authoritative.
defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
}
header, err := Read(reader)
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return nil, ErrNoProxyProtocol
}
return header, err
}
+274 -42
View File
@@ -11,7 +11,12 @@ import (
//
// See below for the different policies.
//
// In case an error is returned the connection is denied.
// In case an error is returned the connection is denied: an error wrapping
// ErrInvalidUpstream denies just that connection while Listener.Accept keeps
// listening; any other error is returned by Accept itself, which typically
// stops the caller's accept loop.
//
// Deprecated: use ConnPolicyFunc instead.
type PolicyFunc func(upstream net.Addr) (Policy, error)
// ConnPolicyFunc can be used to decide whether to trust the PROXY info
@@ -20,7 +25,10 @@ type PolicyFunc func(upstream net.Addr) (Policy, error)
//
// See below for the different policies.
//
// In case an error is returned the connection is denied.
// In case an error is returned the connection is denied: an error wrapping
// ErrInvalidUpstream denies just that connection while Listener.Accept keeps
// listening; any other error is returned by Accept itself, which typically
// stops the caller's accept loop.
type ConnPolicyFunc func(connPolicyOptions ConnPolicyOptions) (Policy, error)
// ConnPolicyOptions contains the remote and local addresses of a connection.
@@ -33,35 +41,67 @@ type ConnPolicyOptions struct {
type Policy int
const (
// USE address from PROXY header
// USE address from PROXY header.
USE Policy = iota
// IGNORE address from PROXY header, but accept connection
// IGNORE address from PROXY header, but accept connection.
IGNORE
// REJECT connection when PROXY header is sent
// Note: even though the first read on the connection returns an error if
// a PROXY header is present, subsequent reads do not. It is the task of
// the code using the connection to handle that case properly.
// Note: if a PROXY header is present the first read returns
// ErrSuperfluousProxyHeader, and every subsequent read returns the same
// error, so the connection should be closed.
REJECT
// REQUIRE connection to send PROXY header, reject if not present
// Note: even though the first read on the connection returns an error if
// 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.
// Note: if no PROXY header is present the first read returns
// ErrNoProxyProtocol, and every subsequent read returns the same error,
// so the connection should be closed.
REQUIRE
// SKIP accepts a connection without requiring the PROXY header
// SKIP accepts a connection without requiring the PROXY header.
// Note: an example usage can be found in the SkipProxyHeaderForCIDR
// function.
//
// On a Listener, SKIP short-circuits Accept and returns the raw, unwrapped
// connection. On a Conn (via WithPolicy), a PROXY header that is present is
// still consumed from the stream but discarded: ProxyHeader returns nil and
// no validation runs.
SKIP
)
// SkipProxyHeaderForCIDR returns a PolicyFunc which can be used to accept a
// connection from a skipHeaderCIDR without requiring a PROXY header, e.g.
// DefaultPolicy is the policy applied when none is configured explicitly: by
// NewConn when no WithPolicy option is given, and by Listener.Accept when
// neither Policy nor ConnPolicy is set.
//
// It defaults to REQUIRE, per the spec's mandate that a receiver "MUST not try
// to guess whether the protocol header is present or not": a connection that
// does not open with a PROXY header fails its first Read/Write with
// ErrNoProxyProtocol. Note REQUIRE alone still honors headers from any peer;
// restricting who may send one needs a policy such as TrustProxyHeaderFrom or
// TrustProxyHeaderFromRanges. (The deprecated *WhiteListPolicy helpers are
// NOT equivalent: they return USE for allowed peers, which replaces this
// default and makes the header optional again — that is why they were
// deprecated in favor of the explicit PolicyFromRanges.)
//
// Deployments that relied on the historical optional-header behavior can
// restore it process-wide with:
//
// proxyproto.DefaultPolicy = proxyproto.USE
//
// Like DefaultReadHeaderTimeout, this is a package-level variable to keep it
// easy to override. Set it at program init; it must not be modified
// concurrently with accepting connections.
var DefaultPolicy = REQUIRE
// ConnSkipProxyHeaderForCIDR returns a ConnPolicyFunc 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)
func ConnSkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) ConnPolicyFunc {
return func(connOpts ConnPolicyOptions) (Policy, error) {
ip, err := ipFromAddr(connOpts.Upstream)
if err != nil {
return def, err
// Deny only this connection: wrapping ErrInvalidUpstream keeps
// Listener.Accept listening instead of surfacing the error and
// stopping the caller's accept loop.
return def, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
}
if skipHeaderCIDR != nil && skipHeaderCIDR.Contains(ip) {
@@ -72,31 +112,66 @@ func SkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) PolicyFunc {
}
}
// WithPolicy adds given policy to a connection when passed as option to NewConn()
// 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.
//
// Deprecated: use ConnSkipProxyHeaderForCIDR instead.
func SkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) PolicyFunc {
connPolicy := ConnSkipProxyHeaderForCIDR(skipHeaderCIDR, def)
return func(upstream net.Addr) (Policy, error) {
return connPolicy(ConnPolicyOptions{Upstream: upstream})
}
}
// WithPolicy adds given policy to a connection when passed as option to NewConn().
func WithPolicy(p Policy) func(*Conn) {
return func(c *Conn) {
c.ProxyHeaderPolicy = p
}
}
// ConnLaxWhiteListPolicy returns a ConnPolicyFunc which decides whether the
// upstream ip is allowed to send a proxy header based on a list of allowed
// IP addresses and IP ranges. In case upstream IP is not in list the proxy
// header will be ignored. If one of the provided IP addresses or IP ranges
// is invalid it will return an error instead of a ConnPolicyFunc.
//
// Deprecated: the name hides that the header stays optional (USE) for allowed
// peers, overriding the REQUIRE default. Use the equivalent, explicit
// PolicyFromRanges(allowed, USE, IGNORE), or TrustProxyHeaderFromRanges for
// the spec-strict header-mandatory posture.
func ConnLaxWhiteListPolicy(allowed []string) (ConnPolicyFunc, error) {
return PolicyFromRanges(allowed, USE, IGNORE)
}
// LaxWhiteListPolicy returns a PolicyFunc which decides whether the
// upstream ip is allowed to send a proxy header based on a list of allowed
// IP addresses and IP ranges. In case upstream IP is not in list the proxy
// header will be ignored. If one of the provided IP addresses or IP ranges
// is invalid it will return an error instead of a PolicyFunc.
//
// Deprecated: use PolicyFromRanges(allowed, USE, IGNORE) instead; see
// ConnLaxWhiteListPolicy for why.
func LaxWhiteListPolicy(allowed []string) (PolicyFunc, error) {
allowFrom, err := parse(allowed)
connPolicy, err := ConnLaxWhiteListPolicy(allowed)
if err != nil {
return nil, err
}
return whitelistPolicy(allowFrom, IGNORE), nil
return func(upstream net.Addr) (Policy, error) {
return connPolicy(ConnPolicyOptions{Upstream: upstream})
}, nil
}
// MustLaxWhiteListPolicy returns a LaxWhiteListPolicy but will panic if one
// of the provided IP addresses or IP ranges is invalid.
func MustLaxWhiteListPolicy(allowed []string) PolicyFunc {
pfunc, err := LaxWhiteListPolicy(allowed)
// ConnMustLaxWhiteListPolicy returns a ConnLaxWhiteListPolicy but will panic
// if one of the provided IP addresses or IP ranges is invalid.
//
// Deprecated: use MustPolicyFromRanges(allowed, USE, IGNORE) instead; see
// ConnLaxWhiteListPolicy for why.
func ConnMustLaxWhiteListPolicy(allowed []string) ConnPolicyFunc {
pfunc, err := ConnLaxWhiteListPolicy(allowed)
if err != nil {
panic(err)
}
@@ -104,26 +179,63 @@ func MustLaxWhiteListPolicy(allowed []string) PolicyFunc {
return pfunc
}
// MustLaxWhiteListPolicy returns a LaxWhiteListPolicy but will panic if one
// of the provided IP addresses or IP ranges is invalid.
//
// Deprecated: use MustPolicyFromRanges(allowed, USE, IGNORE) instead; see
// ConnLaxWhiteListPolicy for why.
func MustLaxWhiteListPolicy(allowed []string) PolicyFunc {
connPolicy := ConnMustLaxWhiteListPolicy(allowed)
return func(upstream net.Addr) (Policy, error) {
return connPolicy(ConnPolicyOptions{Upstream: upstream})
}
}
// ConnStrictWhiteListPolicy returns a ConnPolicyFunc which decides whether the
// upstream ip is allowed to send a proxy header based on a list of allowed
// IP addresses and IP ranges. In case upstream IP is not in list reading on
// the connection will be refused: the first read returns
// ErrSuperfluousProxyHeader and every subsequent read returns the same error,
// so the connection should be closed. If one of the provided IP addresses or IP
// ranges is invalid it will return an error instead of a ConnPolicyFunc.
//
// Deprecated: "strict" only refers to refusing headers from unlisted peers;
// the header stays optional (USE) for allowed peers, overriding the REQUIRE
// default. Use the equivalent, explicit PolicyFromRanges(allowed, USE,
// REJECT), or TrustProxyHeaderFromRanges for the spec-strict header-mandatory
// posture.
func ConnStrictWhiteListPolicy(allowed []string) (ConnPolicyFunc, error) {
return PolicyFromRanges(allowed, USE, REJECT)
}
// StrictWhiteListPolicy returns a PolicyFunc which decides whether the
// upstream ip is allowed to send a proxy header based on a list of allowed
// IP addresses and IP ranges. In case upstream IP is not in list reading on
// the connection will be refused on the first read. Please note: subsequent
// reads do not error. It is the task of the code using the connection to
// handle that case properly. If one of the provided IP addresses or IP
// the connection will be refused: the first read returns
// ErrSuperfluousProxyHeader and every subsequent read returns the same error,
// so the connection should be closed. If one of the provided IP addresses or IP
// ranges is invalid it will return an error instead of a PolicyFunc.
//
// Deprecated: use PolicyFromRanges(allowed, USE, REJECT) instead; see
// ConnStrictWhiteListPolicy for why.
func StrictWhiteListPolicy(allowed []string) (PolicyFunc, error) {
allowFrom, err := parse(allowed)
connPolicy, err := ConnStrictWhiteListPolicy(allowed)
if err != nil {
return nil, err
}
return whitelistPolicy(allowFrom, REJECT), nil
return func(upstream net.Addr) (Policy, error) {
return connPolicy(ConnPolicyOptions{Upstream: upstream})
}, nil
}
// MustStrictWhiteListPolicy returns a StrictWhiteListPolicy but will panic
// ConnMustStrictWhiteListPolicy returns a ConnStrictWhiteListPolicy but will panic
// if one of the provided IP addresses or IP ranges is invalid.
func MustStrictWhiteListPolicy(allowed []string) PolicyFunc {
pfunc, err := StrictWhiteListPolicy(allowed)
//
// Deprecated: use MustPolicyFromRanges(allowed, USE, REJECT) instead; see
// ConnStrictWhiteListPolicy for why.
func ConnMustStrictWhiteListPolicy(allowed []string) ConnPolicyFunc {
pfunc, err := ConnStrictWhiteListPolicy(allowed)
if err != nil {
panic(err)
}
@@ -131,24 +243,74 @@ func MustStrictWhiteListPolicy(allowed []string) PolicyFunc {
return pfunc
}
func whitelistPolicy(allowed []func(net.IP) bool, def Policy) PolicyFunc {
// MustStrictWhiteListPolicy returns a StrictWhiteListPolicy but will panic
// if one of the provided IP addresses or IP ranges is invalid.
//
// Deprecated: use MustPolicyFromRanges(allowed, USE, REJECT) instead; see
// ConnStrictWhiteListPolicy for why.
func MustStrictWhiteListPolicy(allowed []string) PolicyFunc {
connPolicy := ConnMustStrictWhiteListPolicy(allowed)
return func(upstream net.Addr) (Policy, error) {
upstreamIP, err := ipFromAddr(upstream)
return connPolicy(ConnPolicyOptions{Upstream: upstream})
}
}
func connRangesPolicy(allowed []func(net.IP) bool, matched, unmatched Policy) ConnPolicyFunc {
return func(connOpts ConnPolicyOptions) (Policy, error) {
upstreamIP, err := ipFromAddr(connOpts.Upstream)
if err != nil {
// something is wrong with the source IP, better reject the connection
return REJECT, err
// Something is wrong with the source IP: deny only this connection.
// Wrapping ErrInvalidUpstream keeps Listener.Accept listening instead
// of surfacing the error and stopping the caller's accept loop.
return REJECT, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
}
for _, allowFrom := range allowed {
if allowFrom(upstreamIP) {
return USE, nil
return matched, nil
}
}
return def, nil
return unmatched, nil
}
}
// PolicyFromRanges returns a ConnPolicyFunc that applies the matched policy to
// connections whose upstream address belongs to ranges, and the unmatched
// policy to every other connection. Each entry in ranges may be an individual
// IP address ("10.0.0.10") or a CIDR range ("10.0.0.0/24"); an invalid entry
// returns an error instead of a ConnPolicyFunc. Connections whose upstream
// address cannot be classified are dropped by Listener.Accept via an
// ErrInvalidUpstream-wrapping error.
//
// It is the explicit replacement for the deprecated *WhiteListPolicy helpers:
//
// PolicyFromRanges(ranges, USE, IGNORE) // ConnLaxWhiteListPolicy
// PolicyFromRanges(ranges, USE, REJECT) // ConnStrictWhiteListPolicy
//
// Both of those combinations leave the header optional for matched peers. For
// the spec-strict posture — trusted sources MUST send a header, everything
// else is dropped — use TrustProxyHeaderFromRanges instead.
func PolicyFromRanges(ranges []string, matched, unmatched Policy) (ConnPolicyFunc, error) {
allowFrom, err := parse(ranges)
if err != nil {
return nil, err
}
return connRangesPolicy(allowFrom, matched, unmatched), nil
}
// MustPolicyFromRanges returns PolicyFromRanges and panics if any entry in
// ranges is invalid. Intended for static configuration known at program init.
func MustPolicyFromRanges(ranges []string, matched, unmatched Policy) ConnPolicyFunc {
pfunc, err := PolicyFromRanges(ranges, matched, unmatched)
if err != nil {
panic(err)
}
return pfunc
}
func parse(allowed []string) ([]func(net.IP) bool, error) {
a := make([]func(net.IP) bool, len(allowed))
for i, allowFrom := range allowed {
@@ -186,15 +348,85 @@ func ipFromAddr(upstream net.Addr) (net.IP, error) {
return upstreamIP, nil
}
// IgnoreProxyHeaderNotOnInterface retuns a ConnPolicyFunc which can be used to
// TrustProxyHeaderFrom returns a ConnPolicyFunc implementing the spec's
// receiver posture end to end: connections from trusted IPs MUST carry a PROXY
// header (REQUIRE — absence fails the first Read with ErrNoProxyProtocol, so
// header presence is never guessed), and connections from any other source are
// dropped by Listener.Accept without stopping the listener.
//
// Note the REJECT policy alone cannot provide the second half: REJECT refuses
// connections that DO send a header, but a headerless connection from an
// untrusted peer would still be served raw, sharing the port between PROXY and
// non-PROXY traffic — exactly what the spec's security model forbids.
func TrustProxyHeaderFrom(trustedIPs ...net.IP) ConnPolicyFunc {
return func(connOpts ConnPolicyOptions) (Policy, error) {
ip, err := ipFromAddr(connOpts.Upstream)
if err != nil {
// Deny only this connection: wrapping ErrInvalidUpstream keeps
// Listener.Accept listening instead of surfacing the error and
// stopping the caller's accept loop.
return REJECT, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
}
for _, trustedIP := range trustedIPs {
if trustedIP.Equal(ip) {
return REQUIRE, nil
}
}
return REJECT, fmt.Errorf("%w: %s is not a trusted PROXY sender", ErrInvalidUpstream, ip)
}
}
// TrustProxyHeaderFromRanges is the CIDR-capable variant of
// TrustProxyHeaderFrom, with the same spec posture: connections from the
// trusted set MUST carry a PROXY header (REQUIRE), and connections from any
// other source are dropped by Listener.Accept without stopping the listener.
// Each entry in trusted may be an individual IP address ("10.0.0.10") or a
// CIDR range ("10.0.0.0/24"); an invalid entry returns an error instead of a
// ConnPolicyFunc.
//
// Unlike the *WhiteListPolicy helpers — which make the header optional (USE)
// for allowed peers and merely ignore or refuse the header for denied ones —
// this helper never guesses whether a header is present.
func TrustProxyHeaderFromRanges(trusted []string) (ConnPolicyFunc, error) {
allowFrom, err := parse(trusted)
if err != nil {
return nil, err
}
return func(connOpts ConnPolicyOptions) (Policy, error) {
ip, err := ipFromAddr(connOpts.Upstream)
if err != nil {
// Deny only this connection: wrapping ErrInvalidUpstream keeps
// Listener.Accept listening instead of surfacing the error and
// stopping the caller's accept loop.
return REJECT, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
}
for _, allow := range allowFrom {
if allow(ip) {
return REQUIRE, nil
}
}
return REJECT, fmt.Errorf("%w: %s is not a trusted PROXY sender", ErrInvalidUpstream, ip)
}, nil
}
// IgnoreProxyHeaderNotOnInterface returns a ConnPolicyFunc which can be used to
// decide whether to use or ignore PROXY headers depending on the connection
// being made on a specific interface. This policy can be used when the server
// is bound to multiple interfaces but wants to allow on only one interface.
// being made on specific interfaces. This policy can be used when the server
// is bound to multiple interfaces but wants to allow on one or more interfaces.
func IgnoreProxyHeaderNotOnInterface(allowedIP net.IP) ConnPolicyFunc {
return func(connOpts ConnPolicyOptions) (Policy, error) {
ip, err := ipFromAddr(connOpts.Downstream)
if err != nil {
return REJECT, err
// The local (downstream) address cannot be classified; deny only this
// connection. Wrapping ErrInvalidUpstream keeps Listener.Accept
// listening instead of surfacing the error and stopping the caller's
// accept loop.
return REJECT, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
}
if allowedIP.Equal(ip) {
+237 -72
View File
@@ -11,15 +11,30 @@ import (
"time"
)
// readBufferSize is the size used for bufio.Reader's internal buffer.
//
// This is kept low to reduce per-connection memory overhead. If the header is
// larger than readBufferSize, the header will be decoded with multiple Read
// calls. For v1 the header length is at most 108 bytes. For v2 the header
// length is at most 52 bytes plus the length of the TLVs. We use 256 bytes to
// accommodate for the most common cases.
const readBufferSize = 256
var (
// 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"
// e.g. go build -ldflags -X "github.com/pires/go-proxyproto.DefaultReadHeaderTimeout=1s".
DefaultReadHeaderTimeout = 10 * time.Second
// ErrInvalidUpstream should be returned when an upstream connection address
// is not trusted, and therefore is invalid.
// ErrInvalidUpstream should be returned (possibly wrapped) by a policy
// function when an upstream connection address is not trusted or cannot be
// classified. Listener.Accept closes that connection and keeps listening;
// a policy error that does not wrap ErrInvalidUpstream is returned by
// Accept itself, which typically stops the caller's accept loop. All
// built-in policies wrap address-classification failures in
// ErrInvalidUpstream so a single unclassifiable peer cannot stop the
// listener.
ErrInvalidUpstream = fmt.Errorf("proxyproto: upstream connection address not trusted for PROXY information")
)
@@ -31,15 +46,41 @@ var (
// is set, a default of 10s will be used. This can be disabled by setting the
// timeout to < 0.
//
// When neither Policy nor ConnPolicy is set, DefaultPolicy applies: REQUIRE,
// so connections that do not open with a PROXY header fail their first
// Read/Write with ErrNoProxyProtocol. Headers are still honored from ANY peer
// under REQUIRE; a listener reachable by untrusted clients should set a
// trusted-source policy (e.g. TrustProxyHeaderFrom or TrustProxyHeaderFromRanges).
//
// Listener is stream-oriented (TCP, Unix stream): the header is read once at
// the start of the byte stream. It cannot implement the PROXY protocol over
// UDP, where the spec requires a header parsed independently in each datagram;
// use ParseUDPDatagram and Header.FormatUDPDatagram for that.
//
// Note that ReadHeaderTimeout only bounds how long a single slow connection can
// hold a goroutine and file descriptor during header detection; it is not a
// connection count or accept-rate limit. Deployments exposed to untrusted
// clients should keep ReadHeaderTimeout low and enforce connection/rate limits
// upstream (or around Accept).
//
// Only one of Policy or ConnPolicy should be provided. If both are provided then
// a panic would occur during accept.
type Listener struct {
// Listener is the underlying listener.
Listener net.Listener
// Deprecated: use ConnPolicyFunc instead. This will be removed in future release.
Policy PolicyFunc
ConnPolicy ConnPolicyFunc
ValidateHeader Validator
Policy PolicyFunc
// ConnPolicy is the policy function for accepted connections.
ConnPolicy ConnPolicyFunc
// ValidateHeader is the validator function for the proxy header.
ValidateHeader Validator
// ReadHeaderTimeout is the timeout for reading the proxy header.
ReadHeaderTimeout time.Duration
// ReadBufferSize is the read buffer size for accepted connections. When > 0,
// each accepted connection uses this size for proxy header detection; 0 means default.
// See the sizing note on WithBufferSize: values below 107 bytes break v1
// header parsing.
ReadBufferSize int
}
// Conn is used to wrap and underlying connection which
@@ -47,12 +88,13 @@ type Listener struct {
// 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 {
readDeadline atomic.Value // time.Time
once sync.Once
readErr error
conn net.Conn
bufReader *bufio.Reader
reader io.Reader
readDeadline atomic.Value // time.Time
once sync.Once
readErr error
conn net.Conn
bufReader *bufio.Reader
// bufferSize is set when the client overrides via WithBufferSize; nil means use default.
bufferSize *int
header *Header
ProxyHeaderPolicy Policy
Validate Validator
@@ -63,7 +105,7 @@ type Conn struct {
// In case the header is not deemed valid it should return an error.
type Validator func(*Header) error
// ValidateHeader adds given validator for proxy headers to a connection when passed as option to NewConn()
// ValidateHeader adds given validator for proxy headers to a connection when passed as option to NewConn().
func ValidateHeader(v Validator) func(*Conn) {
return func(c *Conn) {
if v != nil {
@@ -72,7 +114,9 @@ func ValidateHeader(v Validator) func(*Conn) {
}
}
// SetReadHeaderTimeout sets the readHeaderTimeout for a connection when passed as option to NewConn()
// SetReadHeaderTimeout sets the readHeaderTimeout for a connection when passed as option to NewConn().
// A value of 0 disables the header read timeout; negative values are ignored,
// leaving the connection's current timeout (the NewConn default) in place.
func SetReadHeaderTimeout(t time.Duration) func(*Conn) {
return func(c *Conn) {
if t >= 0 {
@@ -81,16 +125,38 @@ func SetReadHeaderTimeout(t time.Duration) func(*Conn) {
}
}
// WithBufferSize sets the size of the read buffer used for proxy header detection.
// Values <= 0 are ignored and the default (256 bytes) is used. Values < 16 are
// effectively 16 due to bufio's minimum. The default is tuned for typical proxy
// protocol header lengths.
//
// The buffer must be able to hold an entire v1 header line (up to 107 bytes):
// v1 parsing requires the line to be available without refilling the buffer
// (the slow-loris defense), so a smaller buffer rejects every v1 connection
// with ErrCantReadVersion1Header even from well-behaved senders. v2 parsing
// refills freely and works with any size.
func WithBufferSize(length int) func(*Conn) {
return func(c *Conn) {
if length <= 0 {
return
}
p := new(int)
*p = length
c.bufferSize = p
c.bufReader = bufio.NewReaderSize(c.conn, length)
}
}
// Accept waits for and returns the next valid connection to the listener.
func (p *Listener) Accept() (net.Conn, error) {
for {
// Get the underlying connection
// Get the underlying connection.
conn, err := p.Listener.Accept()
if err != nil {
return nil, err
}
proxyHeaderPolicy := USE
proxyHeaderPolicy := DefaultPolicy
if p.Policy != nil && p.ConnPolicy != nil {
panic("only one of policy or connpolicy must be provided.")
}
@@ -104,35 +170,42 @@ func (p *Listener) Accept() (net.Conn, error) {
})
}
if err != nil {
// can't decide the policy, we can't accept the connection
conn.Close()
// can't decide the policy, we can't accept the connection.
if closeErr := conn.Close(); closeErr != nil {
return nil, closeErr
}
if errors.Is(err, ErrInvalidUpstream) {
// keep listening for other connections
// keep listening for other connections.
continue
}
return nil, err
}
// Handle a connection as a regular one
// Handle a connection as a regular one.
if proxyHeaderPolicy == SKIP {
return conn, nil
}
}
newConn := NewConn(
conn,
opts := []func(*Conn){
WithPolicy(proxyHeaderPolicy),
ValidateHeader(p.ValidateHeader),
)
// If the ReadHeaderTimeout for the listener is unset, use the default timeout.
if p.ReadHeaderTimeout == 0 {
p.ReadHeaderTimeout = DefaultReadHeaderTimeout
}
if p.ReadBufferSize > 0 {
opts = append(opts, WithBufferSize(p.ReadBufferSize))
}
newConn := NewConn(conn, opts...)
// Set the readHeaderTimeout of the new conn to the value of the listener
newConn.readHeaderTimeout = p.ReadHeaderTimeout
// Set the readHeaderTimeout of the new conn to the value of the listener,
// falling back to the default when unset. Read into a local rather than
// writing back to the shared Listener: mutating p here races with
// concurrent Accept calls and would silently rewrite the caller's struct.
readHeaderTimeout := p.ReadHeaderTimeout
if readHeaderTimeout == 0 {
readHeaderTimeout = DefaultReadHeaderTimeout
}
newConn.readHeaderTimeout = readHeaderTimeout
return newConn, nil
}
@@ -148,19 +221,40 @@ func (p *Listener) Addr() net.Addr {
return p.Listener.Addr()
}
// NewConn is used to wrap a net.Conn that may be speaking
// the proxy protocol into a proxyproto.Conn
// NewConn is used to wrap a net.Conn that may be speaking the PROXY protocol
// into a proxyproto.Conn.
//
// Conn is stream-oriented; see the note on Listener about the PROXY protocol
// over UDP datagrams.
//
// By default the returned Conn applies DefaultPolicy (REQUIRE, so the peer
// must open with a PROXY header; override with the WithPolicy option) and
// DefaultReadHeaderTimeout (10s) while detecting the PROXY protocol header, so
// a client that connects but never sends data cannot make header detection
// block forever.
//
// The timeout bounds header detection only, not the first Read end-to-end:
// under a non-REQUIRE policy, when no header is present Read falls through to
// a normal read of the underlying connection, which can still block on a
// silent client (pinning a goroutine and file descriptor). For an end-to-end
// bound, set a read deadline on the connection, or keep the REQUIRE policy,
// which makes the first Read fail when no header arrives within the timeout.
//
// Override the timeout with the SetReadHeaderTimeout option; pass
// SetReadHeaderTimeout(0) to disable it entirely.
//
// NOTE: NewConn may interfere with previously set ReadDeadline on the provided net.Conn,
// because it sets a temporary deadline when detecting and reading the PROXY protocol header.
// If you need to enforce a specific ReadDeadline on the connection, be sure to call Conn.SetReadDeadline
// again after NewConn returns, to restore your desired deadline.
func NewConn(conn net.Conn, opts ...func(*Conn)) *Conn {
// For v1 the header length is at most 108 bytes.
// For v2 the header length is at most 52 bytes plus the length of the TLVs.
// We use 256 bytes to be safe.
const bufSize = 256
br := bufio.NewReaderSize(conn, bufSize)
br := bufio.NewReaderSize(conn, readBufferSize)
pConn := &Conn{
bufReader: br,
reader: io.MultiReader(br, conn),
conn: conn,
bufReader: br,
conn: conn,
ProxyHeaderPolicy: DefaultPolicy,
readHeaderTimeout: DefaultReadHeaderTimeout,
}
for _, opt := range opts {
@@ -170,26 +264,53 @@ func NewConn(conn net.Conn, opts ...func(*Conn)) *Conn {
return pConn
}
// Read is check for the proxy protocol header when doing
// the initial scan. If there is an error parsing the header,
// it is returned and the socket is closed.
// Read checks for the proxy protocol header on the first call, then reads
// from the connection. If there is an error processing the header, it is
// returned by this and every subsequent call. The connection is NOT closed by
// this package; the caller should close it.
func (p *Conn) Read(b []byte) (int, error) {
p.once.Do(func() {
p.readErr = p.readHeader()
})
if p.readErr != nil {
return 0, p.readErr
// Ensure header processing runs at most once and surface any errors.
if err := p.ensureHeaderProcessed(); err != nil {
return 0, err
}
return p.reader.Read(b)
// Drain the buffer if it exists and has data.
if p.bufReader != nil {
if p.bufReader.Buffered() > 0 {
n, err := p.bufReader.Read(b)
// Did we empty the buffer?
// Buffering a net.Conn means the buffer doesn't return io.EOF until the connection returns io.EOF.
// Therefore, we use Buffered() == 0 to detect if we are done with the buffer.
if p.bufReader.Buffered() == 0 {
// Garbage collect the buffer.
p.bufReader = nil
}
// Return immediately. Do not touch p.conn.
// If err is EOF here, it means the connection is actually closed,
// so we should return that error to the user anyway.
return n, err
}
// If buffer was empty to begin with (shouldn't happen with the >0 check
// but good for safety), clear it.
p.bufReader = nil
}
// From now on, read directly from the underlying connection.
return p.conn.Read(b)
}
// Write wraps original conn.Write
// Write wraps original conn.Write.
func (p *Conn) Write(b []byte) (int, error) {
// Ensure header processing has completed before writing.
if err := p.ensureHeaderProcessed(); err != nil {
return 0, err
}
return p.conn.Write(b)
}
// Close wraps original conn.Close
// Close wraps original conn.Close.
func (p *Conn) Close() error {
return p.conn.Close()
}
@@ -197,7 +318,8 @@ func (p *Conn) Close() error {
// ProxyHeader returns the proxy protocol header, if any. If an error occurs
// while reading the proxy header, nil is returned.
func (p *Conn) ProxyHeader() *Header {
p.once.Do(func() { p.readErr = p.readHeader() })
// Ensure header processing runs at most once.
_ = p.ensureHeaderProcessed()
return p.header
}
@@ -208,8 +330,9 @@ func (p *Conn) ProxyHeader() *Header {
// from the proxy header even if the proxy header itself is
// syntactically correct.
func (p *Conn) LocalAddr() net.Addr {
p.once.Do(func() { p.readErr = p.readHeader() })
if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil {
// Ensure header processing runs at most once.
_ = p.ensureHeaderProcessed()
if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil || p.header.DestinationAddr == nil {
return p.conn.LocalAddr()
}
@@ -223,8 +346,9 @@ func (p *Conn) LocalAddr() net.Addr {
// from the proxy header even if the proxy header itself is
// syntactically correct.
func (p *Conn) RemoteAddr() net.Addr {
p.once.Do(func() { p.readErr = p.readHeader() })
if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil {
// Ensure header processing runs at most once.
_ = p.ensureHeaderProcessed()
if p.header == nil || p.header.Command.IsLocal() || p.readErr != nil || p.header.SourceAddr == nil {
return p.conn.RemoteAddr()
}
@@ -266,13 +390,13 @@ func (p *Conn) UDPConn() (conn *net.UDPConn, ok bool) {
return
}
// SetDeadline wraps original conn.SetDeadline
// 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
// 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
@@ -281,18 +405,33 @@ func (p *Conn) SetReadDeadline(t time.Time) error {
return p.conn.SetReadDeadline(t)
}
// SetWriteDeadline wraps original conn.SetWriteDeadline
// SetWriteDeadline wraps original conn.SetWriteDeadline.
func (p *Conn) SetWriteDeadline(t time.Time) error {
return p.conn.SetWriteDeadline(t)
}
// readHeader reads the proxy protocol header from the connection.
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.
// apply a temporary deadline without extending a user-configured
// deadline. If the user has no deadline, we use now + timeout.
if p.readHeaderTimeout > 0 {
if err := p.conn.SetReadDeadline(time.Now().Add(p.readHeaderTimeout)); err != nil {
var (
storedDeadline time.Time
hasDeadline bool
)
if t := p.readDeadline.Load(); t != nil {
storedDeadline = t.(time.Time)
hasDeadline = !storedDeadline.IsZero()
}
headerDeadline := time.Now().Add(p.readHeaderTimeout)
if hasDeadline && storedDeadline.Before(headerDeadline) {
// Clamp to the user's earlier deadline to avoid extending it.
headerDeadline = storedDeadline
}
if err := p.conn.SetReadDeadline(headerDeadline); err != nil {
return err
}
}
@@ -301,7 +440,7 @@ func (p *Conn) readHeader() error {
// 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.
// SetReadDeadline override, we can restore the user's deadline (if any).
// 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 {
@@ -309,11 +448,17 @@ func (p *Conn) readHeader() error {
if t == nil {
t = time.Time{}
}
if err := p.conn.SetReadDeadline(t.(time.Time)); err != nil {
return err
}
// Restore the user's deadline on a best-effort basis. This must not
// discard a header we already parsed: some connections (notably
// net.Pipe) return an error from SetReadDeadline once the peer has
// closed, which can happen right after the peer sends the header and
// closes. Only surface a restore failure when the read produced neither
// a header nor an error of its own.
restoreErr := p.conn.SetReadDeadline(t.(time.Time))
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
err = ErrNoProxyProtocol
} else if err == nil && header == nil {
err = restoreErr
}
}
@@ -349,24 +494,44 @@ func (p *Conn) readHeader() error {
return err
}
// ReadFrom implements the io.ReaderFrom ReadFrom method
// ensureHeaderProcessed runs header processing once.
func (p *Conn) ensureHeaderProcessed() error {
p.once.Do(func() {
p.readErr = p.readHeader()
})
if p.readErr != nil {
return p.readErr
}
return nil
}
// ReadFrom implements the io.ReaderFrom ReadFrom method.
func (p *Conn) ReadFrom(r io.Reader) (int64, error) {
// Ensure header processing has completed before reading/writing.
if err := p.ensureHeaderProcessed(); err != nil {
return 0, err
}
if rf, ok := p.conn.(io.ReaderFrom); ok {
return rf.ReadFrom(r)
}
return io.Copy(p.conn, r)
}
// WriteTo implements io.WriterTo
// WriteTo implements io.WriterTo.
func (p *Conn) WriteTo(w io.Writer) (int64, error) {
p.once.Do(func() { p.readErr = p.readHeader() })
if p.readErr != nil {
return 0, p.readErr
// Ensure header processing has completed before reading/writing.
if err := p.ensureHeaderProcessed(); err != nil {
return 0, err
}
// If the buffer has been drained (or cleared), copy directly from conn.
if p.bufReader == nil {
return io.Copy(w, p.conn)
}
b := make([]byte, p.bufReader.Buffered())
if _, err := p.bufReader.Read(b); err != nil {
return 0, err // this should never as we read buffered data
return 0, err // this should never happen as we read buffered data.
}
var n int64
+41 -26
View File
@@ -1,5 +1,5 @@
// Type-Length-Value splitting and parsing for proxy protocol V2
// See spec https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt sections 2.2 to 2.7 and
// Type-Length-Value splitting and parsing for proxy protocol V2.
// See spec https://www.haproxy.org/download/3.4/doc/proxy-protocol.txt sections 2.2 to 2.2.8.
package proxyproto
@@ -10,22 +10,28 @@ import (
"math"
)
// TLV type constants defined by the PROXY protocol spec.
//
//nolint:revive // Names follow the spec.
const (
// Section 2.2
PP2_TYPE_ALPN PP2Type = 0x01
PP2_TYPE_AUTHORITY PP2Type = 0x02
PP2_TYPE_CRC32C PP2Type = 0x03
PP2_TYPE_NOOP PP2Type = 0x04
PP2_TYPE_UNIQUE_ID PP2Type = 0x05
PP2_TYPE_SSL PP2Type = 0x20
PP2_SUBTYPE_SSL_VERSION PP2Type = 0x21
PP2_SUBTYPE_SSL_CN PP2Type = 0x22
PP2_SUBTYPE_SSL_CIPHER PP2Type = 0x23
PP2_SUBTYPE_SSL_SIG_ALG PP2Type = 0x24
PP2_SUBTYPE_SSL_KEY_ALG PP2Type = 0x25
PP2_TYPE_NETNS PP2Type = 0x30
// Section 2.2.
PP2_TYPE_ALPN PP2Type = 0x01
PP2_TYPE_AUTHORITY PP2Type = 0x02
PP2_TYPE_CRC32C PP2Type = 0x03
PP2_TYPE_NOOP PP2Type = 0x04
PP2_TYPE_UNIQUE_ID PP2Type = 0x05
PP2_TYPE_SSL PP2Type = 0x20
PP2_SUBTYPE_SSL_VERSION PP2Type = 0x21
PP2_SUBTYPE_SSL_CN PP2Type = 0x22
PP2_SUBTYPE_SSL_CIPHER PP2Type = 0x23
PP2_SUBTYPE_SSL_SIG_ALG PP2Type = 0x24
PP2_SUBTYPE_SSL_KEY_ALG PP2Type = 0x25
PP2_SUBTYPE_SSL_GROUP PP2Type = 0x26
PP2_SUBTYPE_SSL_SIG_SCHEME PP2Type = 0x27
PP2_SUBTYPE_SSL_CLIENT_CERT PP2Type = 0x28
PP2_TYPE_NETNS PP2Type = 0x30
// Section 2.2.7, reserved types
// Section 2.2.8, reserved types.
PP2_TYPE_MIN_CUSTOM PP2Type = 0xE0
PP2_TYPE_MAX_CUSTOM PP2Type = 0xEF
PP2_TYPE_MIN_EXPERIMENT PP2Type = 0xF0
@@ -35,15 +41,18 @@ const (
)
var (
ErrTruncatedTLV = errors.New("proxyproto: truncated TLV")
ErrMalformedTLV = errors.New("proxyproto: malformed TLV Value")
// ErrTruncatedTLV indicates a TLV was truncated.
ErrTruncatedTLV = errors.New("proxyproto: truncated TLV")
// ErrMalformedTLV indicates a TLV has malformed data.
ErrMalformedTLV = errors.New("proxyproto: malformed TLV Value")
// ErrIncompatibleTLV indicates a TLV is of an unexpected type.
ErrIncompatibleTLV = errors.New("proxyproto: incompatible TLV type")
)
// PP2Type is the proxy protocol v2 type
// PP2Type is the proxy protocol v2 type.
type PP2Type byte
// TLV is a uninterpreted Type-Length-Value for V2 protocol, see section 2.2
// TLV is a uninterpreted Type-Length-Value for V2 protocol, see section 2.2.
type TLV struct {
Type PP2Type
Value []byte
@@ -83,7 +92,9 @@ func JoinTLVs(tlvs []TLV) ([]byte, error) {
return nil, fmt.Errorf("proxyproto: cannot format TLV %v with length %d", tlv.Type, len(tlv.Value))
}
var length [2]byte
binary.BigEndian.PutUint16(length[:], uint16(len(tlv.Value)))
//nolint:gosec // lengthValue is validated above.
lengthValue := uint16(len(tlv.Value))
binary.BigEndian.PutUint16(length[:], lengthValue)
raw = append(raw, byte(tlv.Type))
raw = append(raw, length[:]...)
raw = append(raw, tlv.Value...)
@@ -91,7 +102,7 @@ func JoinTLVs(tlvs []TLV) ([]byte, error) {
return raw, nil
}
// Registered is true if the type is registered in the spec, see section 2.2
// Registered is true if the type is registered in the spec, see section 2.2.
func (p PP2Type) Registered() bool {
switch p {
case PP2_TYPE_ALPN,
@@ -105,28 +116,32 @@ func (p PP2Type) Registered() bool {
PP2_SUBTYPE_SSL_CIPHER,
PP2_SUBTYPE_SSL_SIG_ALG,
PP2_SUBTYPE_SSL_KEY_ALG,
PP2_SUBTYPE_SSL_GROUP,
PP2_SUBTYPE_SSL_SIG_SCHEME,
PP2_SUBTYPE_SSL_CLIENT_CERT,
PP2_TYPE_NETNS:
return true
}
return false
}
// App is true if the type is reserved for application specific data, see section 2.2.7
// App is true if the type is reserved for application specific data, see section 2.2.8.
func (p PP2Type) App() bool {
return p >= PP2_TYPE_MIN_CUSTOM && p <= PP2_TYPE_MAX_CUSTOM
}
// Experiment is true if the type is reserved for temporary experimental use by application developers, see section 2.2.7
// Experiment is true if the type is reserved for temporary experimental use by application
// developers, see section 2.2.8.
func (p PP2Type) Experiment() bool {
return p >= PP2_TYPE_MIN_EXPERIMENT && p <= PP2_TYPE_MAX_EXPERIMENT
}
// Future is true is the type is reserved for future use, see section 2.2.7
// Future is true is the type is reserved for future use, see section 2.2.8.
func (p PP2Type) Future() bool {
return p >= PP2_TYPE_MIN_FUTURE
}
// Spec is true if the type is covered by the spec, see section 2.2 and 2.2.7
// Spec is true if the type is covered by the spec, see section 2.2 and 2.2.8.
func (p PP2Type) Spec() bool {
return p.Registered() || p.App() || p.Experiment() || p.Future()
}
+49
View File
@@ -0,0 +1,49 @@
package proxyproto
import (
"bufio"
"bytes"
)
// ParseUDPDatagram parses a PROXY protocol header at the start of a UDP
// datagram and returns the header together with the proxied payload that
// follows it. The returned payload aliases the datagram slice.
//
// Per spec (section 2), when the PROXY protocol is carried over UDP the header
// and the proxied payload MUST be sent in the same datagram, and the receiver
// MUST parse the header independently for each received datagram. Call this
// for every datagram received; there is no connection state to carry over.
// Conn and Listener are stream-oriented and cannot provide those semantics.
//
// A datagram that does not begin with a complete, valid header fails with the
// same errors Read returns — ErrNoProxyProtocol when the signature is absent.
// The spec forbids guessing whether a header is present, so on a receiver
// configured for the PROXY protocol such datagrams must be dropped, not
// treated as raw payload.
func ParseUDPDatagram(datagram []byte) (*Header, []byte, error) {
byteReader := bytes.NewReader(datagram)
// Size the buffer to the whole datagram so the header is fully buffered up
// front: v1 parsing aborts if the header is not available in a single read,
// and a datagram, unlike a stream, can never deliver more bytes later.
// bufio enforces a minimum buffer size internally, covering len == 0.
reader := bufio.NewReaderSize(byteReader, len(datagram))
header, err := Read(reader)
if err != nil {
return nil, nil, err
}
consumed := len(datagram) - reader.Buffered() - byteReader.Len()
return header, datagram[consumed:], nil
}
// FormatUDPDatagram renders the header followed by the proxied payload,
// producing the exact bytes to send as one UDP datagram. Per spec (section 2)
// the header and the payload MUST share a single datagram, which a single
// write of the returned slice guarantees; formatting the header and payload
// separately risks a sender splitting them across datagrams.
func (header *Header) FormatUDPDatagram(payload []byte) ([]byte, error) {
buf, err := header.Format()
if err != nil {
return nil, err
}
return append(buf, payload...), nil
}
+90 -17
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"math"
"net"
"net/netip"
"strconv"
@@ -13,8 +14,29 @@ import (
const (
crlf = "\r\n"
separator = " "
// v1ProtoTCP4 and v1ProtoTCP6 are the PROXY protocol v1 transport-protocol
// tokens for TCP over IPv4 and IPv6 respectively (net has no constants for
// these textual identifiers).
v1ProtoTCP4 = "TCP4"
v1ProtoTCP6 = "TCP6"
)
// V1AcceptIPv4InTCP6 permits plain IPv4 literals in the address fields of a v1
// TCP6 line, promoting them to v4-mapped IPv6 (::ffff:x.x.x.x). Some proxies
// (notably the nginx OSS stream module) emit such lines when the client and
// backend address families differ. The spec forbids them — "the advertised
// protocol family dictates what format to use" — so this compatibility mode is
// disabled by default.
//
// Note the promotion is lossy: round-trip serialization renders the address as
// "::ffff:x.x.x.x" rather than the original "x.x.x.x".
//
// Like DefaultReadHeaderTimeout, this is a package-level variable to keep it
// easy to override. Set it at program init; it must not be modified
// concurrently with parsing.
var V1AcceptIPv4InTCP6 = false
func initVersion1() *Header {
header := new(Header)
header.Version = 1
@@ -71,7 +93,7 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
for {
b, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf(ErrCantReadVersion1Header.Error()+": %v", err)
return nil, fmt.Errorf("%w: %w", ErrCantReadVersion1Header, err)
}
buf = append(buf, b)
if b == '\n' {
@@ -95,8 +117,15 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
return nil, ErrLineMustEndWithCrlf
}
// Check full signature.
// Check full signature. Read dispatches to this parser after peeking only
// the first 5 bytes ("PROXY"), so the first token must still be checked to
// be exactly "PROXY": per spec the line starts with "PROXY" followed by
// exactly one space, and anything else (e.g. "PROXYjunk TCP4 ...") is not a
// v1 header.
tokens := strings.Split(string(buf[:len(buf)-2]), separator)
if tokens[0] != "PROXY" {
return nil, ErrCantReadVersion1Header
}
// Expect at least 2 tokens: "PROXY" and the transport protocol.
if len(tokens) < 2 {
@@ -106,9 +135,9 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
// Read address family and protocol
var transportProtocol AddressFamilyAndProtocol
switch tokens[1] {
case "TCP4":
case v1ProtoTCP4:
transportProtocol = TCPv4
case "TCP6":
case v1ProtoTCP6:
transportProtocol = TCPv6
case "UNKNOWN":
transportProtocol = UNSPEC // doesn't exist in v1 but fits UNKNOWN
@@ -116,8 +145,11 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
return nil, ErrCantReadAddressFamilyAndProtocol
}
// Expect 6 tokens only when UNKNOWN is not present.
if transportProtocol != UNSPEC && len(tokens) < 6 {
// Expect exactly 6 tokens when UNKNOWN is not present. The spec's TCP4/TCP6
// line is "PROXY <proto> <src> <dst> <sport> <dport>"; trailing tokens are
// not permitted. (UNKNOWN is handled leniently below, per spec: the receiver
// must ignore anything up to the CRLF.)
if transportProtocol != UNSPEC && len(tokens) != 6 {
return nil, ErrCantReadAddressFamilyAndProtocol
}
@@ -170,9 +202,9 @@ func (header *Header) formatVersion1() ([]byte, error) {
var proto string
switch header.TransportProtocol {
case TCPv4:
proto = "TCP4"
proto = v1ProtoTCP4
case TCPv6:
proto = "TCP6"
proto = v1ProtoTCP6
default:
// Unknown connection (short form)
return []byte("PROXY UNKNOWN" + crlf), nil
@@ -184,16 +216,35 @@ func (header *Header) formatVersion1() ([]byte, error) {
return nil, ErrInvalidAddress
}
sourceIP, destIP := sourceAddr.IP, destAddr.IP
// A hand-built header can carry any int; the spec requires ports in the
// decimal range 0..65535, so validate before serializing (mirrors the v2
// port check in formatVersion2).
if sourceAddr.Port < 0 || sourceAddr.Port > math.MaxUint16 || destAddr.Port < 0 || destAddr.Port > math.MaxUint16 {
return nil, ErrInvalidPortNumber
}
// netip.Addr (not net.IP) is used here so String() honors the address family
// declared by TransportProtocol. AddrFromSlice reports ok=false when the slice
// is nil (e.g. To4() on an IPv6-only address), which the guard below rejects.
var sourceIP, destIP netip.Addr
switch header.TransportProtocol {
case TCPv4:
sourceIP = sourceIP.To4()
destIP = destIP.To4()
sourceIP, sourceOK = netip.AddrFromSlice(sourceAddr.IP.To4())
destIP, destOK = netip.AddrFromSlice(destAddr.IP.To4())
case TCPv6:
sourceIP = sourceIP.To16()
destIP = destIP.To16()
// Use netip.Addr instead of net.IP to guarantee an Is6() address; i.e. a
// v4-mapped IP in a TCP6 header serializes as ::ffff:1.2.3.4 instead of
// net.IP.String()'s collapsed 1.2.3.4.
sourceIP, sourceOK = netip.AddrFromSlice(sourceAddr.IP.To16())
destIP, destOK = netip.AddrFromSlice(destAddr.IP.To16())
default:
// Unreachable today: the proto switch at the top of this function already
// returns for anything other than TCPv4/TCPv6. Kept so a future protocol
// can't fall through with zero-value IPs while sourceOK/destOK still hold
// from the type assertion above.
return nil, ErrInvalidAddress
}
if sourceIP == nil || destIP == nil {
if !sourceOK || !destOK {
return nil, ErrInvalidAddress
}
@@ -215,16 +266,32 @@ func (header *Header) formatVersion1() ([]byte, error) {
}
func parseV1PortNumber(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
// Per spec, a v1 port is 1..5 decimal digits in the range 0..65535, with no
// leading zero and no sign. ParseUint with bitSize 16 enforces all of that
// (it rejects empty strings, signs, non-digits, and values above 65535)
// except the leading zero ("080"), which the spec forbids and which creates
// ambiguity for anything downstream that re-parses the address.
if len(portStr) > 1 && portStr[0] == '0' {
return 0, ErrInvalidPortNumber
}
return port, nil
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, fmt.Errorf("%w: %w", ErrInvalidPortNumber, err)
}
return int(port), nil
}
func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP, error) {
addr, err := netip.ParseAddr(addrStr)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
}
// netip accepts zoned literals ("fe80::1%eth0"). The spec's address grammar
// does not (hex digits and colons only), and net.IP cannot carry a zone, so
// accepting one would silently forward an address the sender never wrote.
// Per spec, "any sequence which does not exactly match the protocol must be
// discarded".
if addr.Zone() != "" {
return nil, ErrInvalidAddress
}
@@ -237,6 +304,12 @@ func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP
if addr.Is6() || addr.Is4In6() {
return net.IP(addr.AsSlice()), nil
}
// Plain IPv4 in a TCP6 line is spec-invalid but emitted by some proxies;
// see V1AcceptIPv4InTCP6 for the compatibility trade-off.
if V1AcceptIPv4InTCP6 && addr.Is4() {
mapped := netip.AddrFrom16(addr.As16())
return net.IP(mapped.AsSlice()), nil
}
}
return nil, ErrInvalidAddress
+93 -21
View File
@@ -5,10 +5,28 @@ import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"net"
)
// MaxV2HeaderSize is the maximum accepted value of a v2 header's 16-bit length
// field, i.e. the number of bytes following the fixed 16-byte prefix (address
// block plus TLVs).
//
// The spec allows up to 65535, but parseVersion2 allocates this many bytes
// before reading, so a lower limit mitigates memory-allocation DoS from
// untrusted peers while allowing real-world legitimate headers.
// PP2_SUBTYPE_SSL_CLIENT_CERT (a DER-encoded certificate) is typically between
// 1 and 2KiB, so the 4KiB default leaves room for other TLVs; deployments
// expecting larger headers may raise it.
//
// Like DefaultReadHeaderTimeout, this is a package-level variable to keep it
// easy to override. Set it at program init; it must not be modified
// concurrently with parsing.
var MaxV2HeaderSize uint16 = 4096
var (
lengthUnspec = uint16(0)
lengthV4 = uint16(12)
@@ -62,9 +80,9 @@ type _addrUnix struct {
func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
// Skip first 12 bytes (signature)
for i := 0; i < 12; i++ {
for range 12 {
if _, err = reader.ReadByte(); err != nil {
return nil, ErrCantReadProtocolVersionAndCommand
return nil, fmt.Errorf("%w: %w", ErrCantReadProtocolVersionAndCommand, err)
}
}
@@ -74,7 +92,7 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
// Read the 13th byte, protocol version and command
b13, err := reader.ReadByte()
if err != nil {
return nil, ErrCantReadProtocolVersionAndCommand
return nil, fmt.Errorf("%w: %w", ErrCantReadProtocolVersionAndCommand, err)
}
header.Command = ProtocolVersionAndCommand(b13)
if _, ok := supportedCommand[header.Command]; !ok {
@@ -84,20 +102,60 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
// Read the 14th byte, address family and protocol
b14, err := reader.ReadByte()
if err != nil {
return nil, ErrCantReadAddressFamilyAndProtocol
return nil, fmt.Errorf("%w: %w", ErrCantReadAddressFamilyAndProtocol, err)
}
header.TransportProtocol = AddressFamilyAndProtocol(b14)
// UNSPEC is only supported when LOCAL is set.
// Per spec (section 2.2) only the listed address family / transport protocol
// combinations are defined; "other values are unspecified and must not be
// emitted in version 2 of this protocol and must be rejected as invalid by
// receivers". That mandate has no command carve-out, so it applies to LOCAL
// too: the family byte is ignored when interpreting a LOCAL address block,
// but an undefined byte still makes the frame invalid. Rejecting bytes that
// share a known family with an undefined transport — e.g. 0x13 (IPv4
// family, transport 3) — also matters for PROXY: such a byte passes the
// IsIPv4/IsIPv6 checks below, has an address struct read for it, and then
// yields a nil net.Addr from newIPAddr. That nil is stored as
// SourceAddr/DestinationAddr and panics any caller that does
// RemoteAddr().String() (e.g. logging), a remotely triggerable crash.
if !supportedTransportProtocol[header.TransportProtocol] {
return nil, ErrUnsupportedAddressFamilyAndProtocol
}
// UNSPEC carries no address block to trust. For LOCAL it is the family the
// spec expects senders to use; for PROXY the spec leaves it to the receiver
// to accept or reject, and this library rejects it.
if header.TransportProtocol == UNSPEC && header.Command != LOCAL {
return nil, ErrUnsupportedAddressFamilyAndProtocol
}
// Make sure there are bytes available as specified in length
var length uint16
if err := binary.Read(io.LimitReader(reader, 2), binary.BigEndian, &length); err != nil {
return nil, ErrCantReadLength
if err := binary.Read(reader, binary.BigEndian, &length); err != nil {
return nil, fmt.Errorf("%w: %w", ErrCantReadLength, err)
}
if !header.validateLength(length) {
// A LOCAL connection is one the proxy opened itself (e.g. health
// checks). Per spec (section 2.2) the receiver must use the real
// connection endpoints (Conn.RemoteAddr/LocalAddr short-circuit on
// IsLocal), must skip exactly `length` bytes, and "must not assume zero
// is presented for LOCAL connections". So a LOCAL frame whose length
// does not fit its declared family's address-block layout — e.g.
// LOCAL + TCPv4 + length 0 — is still valid: skip the block, which the
// spec says to discard, and normalize the header to UNSPEC so it stays
// serializable via Format/WriteTo. A LOCAL frame whose length does fit
// the family layout is instead decoded exactly like a PROXY frame
// below, preserving the (informational) addresses and any trailing
// TLVs, so the header round-trips byte for byte.
if header.Command == LOCAL {
if length > MaxV2HeaderSize {
return nil, ErrInvalidLength
}
if _, err := reader.Discard(int(length)); err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidLength, err)
}
header.TransportProtocol = UNSPEC
return header, nil
}
return nil, ErrInvalidLength
}
@@ -107,7 +165,7 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
return header, nil
}
if _, err := reader.Peek(int(length)); err != nil {
if length > MaxV2HeaderSize {
return nil, ErrInvalidLength
}
@@ -121,26 +179,26 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
if header.TransportProtocol.IsIPv4() {
var addr _addr4
if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
return nil, ErrInvalidAddress
return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
}
header.SourceAddr = newIPAddr(header.TransportProtocol, addr.Src[:], addr.SrcPort)
header.DestinationAddr = newIPAddr(header.TransportProtocol, addr.Dst[:], addr.DstPort)
} else if header.TransportProtocol.IsIPv6() {
var addr _addr6
if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
return nil, ErrInvalidAddress
return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
}
header.SourceAddr = newIPAddr(header.TransportProtocol, addr.Src[:], addr.SrcPort)
header.DestinationAddr = newIPAddr(header.TransportProtocol, addr.Dst[:], addr.DstPort)
} else if header.TransportProtocol.IsUnix() {
var addr _addrUnix
if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil {
return nil, ErrInvalidAddress
return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
}
network := "unix"
network := networkUnix
if header.TransportProtocol.IsDatagram() {
network = "unixgram"
network = networkUnixgram
}
header.SourceAddr = &net.UnixAddr{
@@ -160,6 +218,10 @@ func parseVersion2(reader *bufio.Reader) (header *Header, err error) {
return nil, err
}
if payloadReader.N != 0 {
return nil, ErrInvalidLength
}
return header, nil
}
@@ -196,7 +258,11 @@ func (header *Header) formatVersion2() ([]byte, error) {
addrSrc = sourceIP.To16()
addrDst = destIP.To16()
} else if header.TransportProtocol.IsUnix() {
buf.Write(lengthUnixBytes)
hdrLen, err := addTLVLen(lengthUnixBytes, len(header.rawTLVs))
if err != nil {
return nil, err
}
buf.Write(hdrLen)
sourceAddr, destAddr, ok := header.UnixAddrs()
if !ok {
return nil, ErrInvalidAddress
@@ -212,11 +278,16 @@ func (header *Header) formatVersion2() ([]byte, error) {
buf.Write(addrDst)
if sourcePort, destPort, ok := header.Ports(); ok {
if sourcePort < 0 || sourcePort > math.MaxUint16 || destPort < 0 || destPort > math.MaxUint16 {
return nil, ErrInvalidPortNumber
}
portBytes := make([]byte, 2)
//nolint:gosec // Bounds are checked above.
binary.BigEndian.PutUint16(portBytes, uint16(sourcePort))
buf.Write(portBytes)
//nolint:gosec // Bounds are checked above.
binary.BigEndian.PutUint16(portBytes, uint16(destPort))
buf.Write(portBytes)
}
@@ -253,6 +324,7 @@ func addTLVLen(cur []byte, tlvLen int) ([]byte, error) {
return nil, errUint16Overflow
}
a := make([]byte, 2)
//nolint:gosec // newLen bounds are validated above.
binary.BigEndian.PutUint16(a, uint16(newLen))
return a, nil
}
@@ -260,19 +332,19 @@ func addTLVLen(cur []byte, tlvLen int) ([]byte, error) {
func newIPAddr(transport AddressFamilyAndProtocol, ip net.IP, port uint16) net.Addr {
if transport.IsStream() {
return &net.TCPAddr{IP: ip, Port: int(port)}
} else if transport.IsDatagram() {
return &net.UDPAddr{IP: ip, Port: int(port)}
} else {
return nil
}
if transport.IsDatagram() {
return &net.UDPAddr{IP: ip, Port: int(port)}
}
return nil
}
func parseUnixName(b []byte) string {
i := bytes.IndexByte(b, 0)
if i < 0 {
before, _, ok := bytes.Cut(b, []byte{0})
if !ok {
return string(b)
}
return string(b[:i])
return string(before)
}
func formatUnixName(name string) []byte {
+2 -1
View File
@@ -33,7 +33,8 @@ func (pvc ProtocolVersionAndCommand) IsProxy() bool {
// IsUnspec returns true if the command is unspecified, false otherwise.
func (pvc ProtocolVersionAndCommand) IsUnspec() bool {
return !(pvc.IsLocal() || pvc.IsProxy())
// Must be LOCAL or PROXY.
return !pvc.IsLocal() && !pvc.IsProxy()
}
func (pvc ProtocolVersionAndCommand) toByte() byte {