439 lines
17 KiB
Go
439 lines
17 KiB
Go
package proxyproto
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
// PolicyFunc can be used to decide whether to trust the PROXY info from
|
|
// upstream. If set, the connecting address is passed in as an argument.
|
|
//
|
|
// See below for the different policies.
|
|
//
|
|
// 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
|
|
// based on connection policy options. If set, the connecting addresses
|
|
// (remote and local) are passed in as argument.
|
|
//
|
|
// See below for the different policies.
|
|
//
|
|
// 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.
|
|
type ConnPolicyOptions struct {
|
|
Upstream net.Addr
|
|
Downstream net.Addr
|
|
}
|
|
|
|
// Policy defines how a connection with a PROXY header address is treated.
|
|
type Policy int
|
|
|
|
const (
|
|
// USE address from PROXY header.
|
|
USE Policy = iota
|
|
// IGNORE address from PROXY header, but accept connection.
|
|
IGNORE
|
|
// REJECT connection when PROXY header is sent
|
|
// 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: 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.
|
|
// 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
|
|
)
|
|
|
|
// 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 ConnSkipProxyHeaderForCIDR(skipHeaderCIDR *net.IPNet, def Policy) 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 def, fmt.Errorf("%w: %w", ErrInvalidUpstream, err)
|
|
}
|
|
|
|
if skipHeaderCIDR != nil && skipHeaderCIDR.Contains(ip) {
|
|
return SKIP, nil
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
connPolicy, err := ConnLaxWhiteListPolicy(allowed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return func(upstream net.Addr) (Policy, error) {
|
|
return connPolicy(ConnPolicyOptions{Upstream: upstream})
|
|
}, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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: 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) {
|
|
connPolicy, err := ConnStrictWhiteListPolicy(allowed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return func(upstream net.Addr) (Policy, error) {
|
|
return connPolicy(ConnPolicyOptions{Upstream: upstream})
|
|
}, nil
|
|
}
|
|
|
|
// ConnMustStrictWhiteListPolicy returns a ConnStrictWhiteListPolicy 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 ConnMustStrictWhiteListPolicy(allowed []string) ConnPolicyFunc {
|
|
pfunc, err := ConnStrictWhiteListPolicy(allowed)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return pfunc
|
|
}
|
|
|
|
// 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) {
|
|
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: 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 matched, 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 {
|
|
if strings.LastIndex(allowFrom, "/") > 0 {
|
|
_, ipRange, err := net.ParseCIDR(allowFrom)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proxyproto: given string %q is not a valid IP range: %v", allowFrom, err)
|
|
}
|
|
|
|
a[i] = ipRange.Contains
|
|
} else {
|
|
allowed := net.ParseIP(allowFrom)
|
|
if allowed == nil {
|
|
return nil, fmt.Errorf("proxyproto: given string %q is not a valid IP address", allowFrom)
|
|
}
|
|
|
|
a[i] = allowed.Equal
|
|
}
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
func ipFromAddr(upstream net.Addr) (net.IP, error) {
|
|
upstreamString, _, err := net.SplitHostPort(upstream.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
upstreamIP := net.ParseIP(upstreamString)
|
|
if nil == upstreamIP {
|
|
return nil, fmt.Errorf("proxyproto: invalid IP address")
|
|
}
|
|
|
|
return upstreamIP, nil
|
|
}
|
|
|
|
// 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 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 {
|
|
// 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) {
|
|
return USE, nil
|
|
}
|
|
|
|
return IGNORE, nil
|
|
}
|
|
}
|