diff --git a/go.mod b/go.mod index 077e83437ad..5749618e100 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/klauspost/compress v1.15.6 github.com/klauspost/cpuid/v2 v2.0.13 github.com/lucas-clemente/quic-go v0.27.2 + github.com/mastercactapus/proxyprotocol v0.0.3 github.com/mholt/acmez v1.0.2 github.com/prometheus/client_golang v1.12.1 github.com/smallstep/certificates v0.19.0 diff --git a/go.sum b/go.sum index 56ae7cab2ab..ee74abf1879 100644 --- a/go.sum +++ b/go.sum @@ -741,6 +741,8 @@ github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/mastercactapus/proxyprotocol v0.0.3 h1:WpDMFKCYdF8NsoA6OrXyNKyZrzMURqqOP1PE7297RCE= +github.com/mastercactapus/proxyprotocol v0.0.3/go.mod h1:X8FRVEDZz9FkrIoL4QYTBF4Ka4ELwTv0sah0/5NxCPw= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index b2bdf049e34..d122b35e375 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -803,6 +803,7 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // read_buffer // write_buffer // max_response_header +// proxy_protocol v1|v2 // dial_timeout // dial_fallback_delay // response_header_timeout @@ -858,6 +859,17 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } h.MaxResponseHeaderSize = int64(size) + case "proxy_protocol": + if !d.NextArg() { + return d.ArgErr() + } + switch proxyProtocol := d.Val(); proxyProtocol { + case "v1", "v2": + h.ProxyProtocol = proxyProtocol + default: + return d.Errf("invalid proxy protocol version '%s'", proxyProtocol) + } + case "dial_timeout": if !d.NextArg() { return d.ArgErr() diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index a973ecba718..7670c2cf333 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -259,3 +259,22 @@ var hosts = caddy.NewUsagePool() // dialInfoVarKey is the key used for the variable that holds // the dial info for the upstream connection. const dialInfoVarKey = "reverse_proxy.dial_info" + +// ProxyProtocolInfo contains information needed to write proxy protocol to a +// connection to an upstream host. +type ProxyProtocolInfo struct { + // assume network is tcp because golang http transport can only support tcp now + IP net.IP + Port int +} + +// GetProxyProtocolInfo gets the proxy protocol info out of the context, +// and returns true if there was a valid value; false otherwise. +func GetProxyProtocolInfo(ctx context.Context) (ProxyProtocolInfo, bool) { + proxyProtocolInfo, ok := caddyhttp.GetVar(ctx, proxyProtocolInfoVarKey).(ProxyProtocolInfo) + return proxyProtocolInfo, ok +} + +// proxyProtocolInfoVarKey is the key used for the variable that holds +// the proxy protocol info for the upstream connection. +const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info" diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 94a09380ce8..0ee9c99ce54 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -30,6 +30,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/mastercactapus/proxyprotocol" "golang.org/x/net/http2" ) @@ -63,6 +64,10 @@ type HTTPTransport struct { // Maximum number of connections per host. Default: 0 (no limit) MaxConnsPerHost int `json:"max_conns_per_host,omitempty"` + // Which version of proxy protocol to send when connecting to + // an upstream. Default: `don't send proxy protocol`. + ProxyProtocol string `json:"proxy_protocol,omitempty"` + // How long to wait before timing out trying to connect to // an upstream. Default: `3s`. DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` @@ -201,6 +206,33 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) // decide whether to retry a request return nil, DialError{err} } + + if proxyProtocolInfo, ok := GetProxyProtocolInfo(ctx); ok { + switch h.ProxyProtocol { + case "": + return conn, nil + case "v1": + var header proxyprotocol.HeaderV1 + header.FromConn(conn, true) + header.SrcIP = proxyProtocolInfo.IP + header.SrcPort = proxyProtocolInfo.Port + _, err = header.WriteTo(conn) + case "v2": + var header proxyprotocol.HeaderV2 + header.FromConn(conn, true) + header.Src = &net.TCPAddr{ + IP: proxyProtocolInfo.IP, + Port: proxyProtocolInfo.Port, + } + _, err = header.WriteTo(conn) + } + if err != nil { + // identify this error as one that occurred during + // dialing, which can be important when trying to + // decide whether to retry a request + return nil, DialError{err} + } + } return conn, nil }, MaxConnsPerHost: h.MaxConnsPerHost, diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 15e310422da..1da59b6bcef 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -502,6 +502,33 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // or satisfactorily represented in a URL caddyhttp.SetVar(r.Context(), dialInfoVarKey, dialInfo) + var proxyProtocolInfo ProxyProtocolInfo + // using X-Forwarded-For header which is already filtered by trusted proxies + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + // addForwardedHeaders ensures xff has at least remoteAddr + xff = strings.TrimSpace(strings.Split(xff, ",")[0]) + ip := net.ParseIP(xff) + if ip != nil { + proxyProtocolInfo.IP = ip + // X-Forwarded-For is set by caddy, not by prefix matching because ipv6 remoteAddr starts with [ + if strings.Contains(r.RemoteAddr, xff) { + // addForwardedHeaders already check this error + _, p, _ := net.SplitHostPort(r.RemoteAddr) + + // however port is never checked + port, err := strconv.Atoi(p) + if err != nil { + return true, fmt.Errorf("making proxy protocol info: %v", err) + } + proxyProtocolInfo.Port = port + } else { + // set to zero for unknown + proxyProtocolInfo.Port = 0 + } + caddyhttp.SetVar(r.Context(), proxyProtocolInfoVarKey, dialInfo) + } + } + // set placeholders with information about this upstream repl.Set("http.reverse_proxy.upstream.address", dialInfo.String()) repl.Set("http.reverse_proxy.upstream.hostport", dialInfo.Address)