Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+35
@@ -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
@@ -1,162 +1,165 @@
|
||||
# go-proxyproto
|
||||
|
||||
[](https://github.com/pires/go-proxyproto/actions)
|
||||
[](https://coveralls.io/github/pires/go-proxyproto?branch=master)
|
||||
[](https://coveralls.io/github/pires/go-proxyproto?branch=main)
|
||||
[](https://goreportcard.com/report/github.com/pires/go-proxyproto)
|
||||
[](https://pkg.go.dev/github.com/pires/go-proxyproto?tab=doc)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user