From dc12bd974367b1c15e6b8f3c6b446a208ac3546f Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 13 Dec 2023 19:07:43 +0300 Subject: [PATCH] proxyprotocol: use github.com/pires/go-proxyproto (#5915) * proxyprotocol: use github.com/pires/go-proxyproto * Fix typo: r/generelly/generally Co-authored-by: Francis Lavoie * add config options for `Deny` CIDR and fallback policy * use `netip` package & trust unix sockets --------- Co-authored-by: Francis Lavoie --- go.mod | 2 +- go.sum | 4 +- .../proxyprotocol/listenerwrapper.go | 72 ++++++++++++---- modules/caddyhttp/proxyprotocol/module.go | 14 +++- modules/caddyhttp/proxyprotocol/policy.go | 82 +++++++++++++++++++ .../caddyhttp/reverseproxy/httptransport.go | 44 +++++----- 6 files changed, 176 insertions(+), 42 deletions(-) create mode 100644 modules/caddyhttp/proxyprotocol/policy.go diff --git a/go.mod b/go.mod index 4a2c40c0f..dde4a27e3 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/google/uuid v1.3.1 github.com/klauspost/compress v1.17.0 github.com/klauspost/cpuid/v2 v2.2.5 - github.com/mastercactapus/proxyprotocol v0.0.4 github.com/mholt/acmez v1.2.0 github.com/prometheus/client_golang v1.15.1 github.com/quic-go/quic-go v0.40.0 @@ -117,6 +116,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pires/go-proxyproto v0.7.0 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect diff --git a/go.sum b/go.sum index 177d1ae0a..adcdf2402 100644 --- a/go.sum +++ b/go.sum @@ -352,8 +352,6 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mastercactapus/proxyprotocol v0.0.4 h1:qSY75IZF30ZqIU9iW1ip3I7gTnm8wRAnGWqPxCBVgq0= -github.com/mastercactapus/proxyprotocol v0.0.4/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.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -433,6 +431,8 @@ github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9 github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= +github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go index f404c06fe..e0d9b86ce 100644 --- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go +++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go @@ -15,11 +15,11 @@ package proxyprotocol import ( - "fmt" "net" + "net/netip" "time" - "github.com/mastercactapus/proxyprotocol" + goproxy "github.com/pires/go-proxyproto" "github.com/caddyserver/caddy/v2" ) @@ -38,32 +38,74 @@ type ListenerWrapper struct { // Allow is an optional list of CIDR ranges to // allow/require PROXY headers from. Allow []string `json:"allow,omitempty"` + allow []netip.Prefix - rules []proxyprotocol.Rule + // Denby is an optional list of CIDR ranges to + // deny PROXY headers from. + Deny []string `json:"deny,omitempty"` + deny []netip.Prefix + + // Accepted values are: ignore, use, reject, require, skip + // default: ignore + // Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy + FallbackPolicy Policy `json:"fallback_policy,omitempty"` + + policy goproxy.PolicyFunc } // Provision sets up the listener wrapper. func (pp *ListenerWrapper) Provision(ctx caddy.Context) error { - rules := make([]proxyprotocol.Rule, 0, len(pp.Allow)) - for _, s := range pp.Allow { - _, n, err := net.ParseCIDR(s) + for _, cidr := range pp.Allow { + ipnet, err := netip.ParsePrefix(cidr) if err != nil { - return fmt.Errorf("invalid subnet '%s': %w", s, err) + return err } - rules = append(rules, proxyprotocol.Rule{ - Timeout: time.Duration(pp.Timeout), - Subnet: n, - }) + pp.allow = append(pp.allow, ipnet) } + for _, cidr := range pp.Deny { + ipnet, err := netip.ParsePrefix(cidr) + if err != nil { + return err + } + pp.deny = append(pp.deny, ipnet) + } + pp.policy = func(upstream net.Addr) (goproxy.Policy, error) { + // trust unix sockets + if network := upstream.Network(); caddy.IsUnixNetwork(network) { + return goproxy.USE, nil + } + ret := pp.FallbackPolicy + host, _, err := net.SplitHostPort(upstream.String()) + if err != nil { + return goproxy.REJECT, err + } - pp.rules = rules - + ip, err := netip.ParseAddr(host) + if err != nil { + return goproxy.REJECT, err + } + for _, ipnet := range pp.deny { + if ipnet.Contains(ip) { + return goproxy.REJECT, nil + } + } + for _, ipnet := range pp.allow { + if ipnet.Contains(ip) { + ret = PolicyUSE + break + } + } + return policyToGoProxyPolicy[ret], nil + } return nil } // WrapListener adds PROXY protocol support to the listener. func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener { - pl := proxyprotocol.NewListener(l, time.Duration(pp.Timeout)) - pl.SetFilter(pp.rules) + pl := &goproxy.Listener{ + Listener: l, + ReadHeaderTimeout: time.Duration(pp.Timeout), + } + pl.Policy = pp.policy return pl } diff --git a/modules/caddyhttp/proxyprotocol/module.go b/modules/caddyhttp/proxyprotocol/module.go index 78f89c650..c6dc283ea 100644 --- a/modules/caddyhttp/proxyprotocol/module.go +++ b/modules/caddyhttp/proxyprotocol/module.go @@ -35,6 +35,8 @@ func (ListenerWrapper) CaddyModule() caddy.ModuleInfo { // proxy_protocol { // timeout // allow +// deny +// fallback_policy // } func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { @@ -57,7 +59,17 @@ func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { case "allow": w.Allow = append(w.Allow, d.RemainingArgs()...) - + case "deny": + w.Deny = append(w.Deny, d.RemainingArgs()...) + case "fallback_policy": + if !d.NextArg() { + return d.ArgErr() + } + p, err := parsePolicy(d.Val()) + if err != nil { + return d.WrapErr(err) + } + w.FallbackPolicy = p default: return d.ArgErr() } diff --git a/modules/caddyhttp/proxyprotocol/policy.go b/modules/caddyhttp/proxyprotocol/policy.go new file mode 100644 index 000000000..6dc8beb45 --- /dev/null +++ b/modules/caddyhttp/proxyprotocol/policy.go @@ -0,0 +1,82 @@ +package proxyprotocol + +import ( + "errors" + "fmt" + "strings" + + goproxy "github.com/pires/go-proxyproto" +) + +type Policy int + +// as defined in: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy +const ( + // IGNORE address from PROXY header, but accept connection + PolicyIGNORE Policy = iota + // USE address from PROXY header + PolicyUSE + // 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. + PolicyREJECT + // 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. + PolicyREQUIRE + // SKIP accepts a connection without requiring the PROXY header + // Note: an example usage can be found in the SkipProxyHeaderForCIDR + // function. + PolicySKIP +) + +var policyToGoProxyPolicy = map[Policy]goproxy.Policy{ + PolicyUSE: goproxy.USE, + PolicyIGNORE: goproxy.IGNORE, + PolicyREJECT: goproxy.REJECT, + PolicyREQUIRE: goproxy.REQUIRE, + PolicySKIP: goproxy.SKIP, +} + +var policyMap = map[Policy]string{ + PolicyUSE: "USE", + PolicyIGNORE: "IGNORE", + PolicyREJECT: "REJECT", + PolicyREQUIRE: "REQUIRE", + PolicySKIP: "SKIP", +} + +var policyMapRev = map[string]Policy{ + "USE": PolicyUSE, + "IGNORE": PolicyIGNORE, + "REJECT": PolicyREJECT, + "REQUIRE": PolicyREQUIRE, + "SKIP": PolicySKIP, +} + +// MarshalText implements the text marshaller method. +func (x Policy) MarshalText() ([]byte, error) { + return []byte(policyMap[x]), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *Policy) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := parsePolicy(name) + if err != nil { + return err + } + *x = tmp + return nil +} + +func parsePolicy(name string) (Policy, error) { + if x, ok := policyMapRev[strings.ToUpper(name)]; ok { + return x, nil + } + return Policy(0), fmt.Errorf("%s is %w", name, errInvalidPolicy) +} + +var errInvalidPolicy = errors.New("invalid policy") diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 9290f7ecb..187bccc66 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -28,7 +28,7 @@ import ( "strings" "time" - "github.com/mastercactapus/proxyprotocol" + "github.com/pires/go-proxyproto" "go.uber.org/zap" "golang.org/x/net/http2" @@ -207,44 +207,42 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e if !ok { return nil, fmt.Errorf("failed to get proxy protocol info from context") } - - // The src and dst have to be of the some address family. As we don't know the original - // dst address (it's kind of impossible to know) and this address is generelly of very + header := proxyproto.Header{ + SourceAddr: &net.TCPAddr{ + IP: proxyProtocolInfo.AddrPort.Addr().AsSlice(), + Port: int(proxyProtocolInfo.AddrPort.Port()), + Zone: proxyProtocolInfo.AddrPort.Addr().Zone(), + }, + } + // The src and dst have to be of the same address family. As we don't know the original + // dst address (it's kind of impossible to know) and this address is generally of very // little interest, we just set it to all zeros. - var destIP net.IP switch { case proxyProtocolInfo.AddrPort.Addr().Is4(): - destIP = net.IPv4zero + header.TransportProtocol = proxyproto.TCPv4 + header.DestinationAddr = &net.TCPAddr{ + IP: net.IPv4zero, + } case proxyProtocolInfo.AddrPort.Addr().Is6(): - destIP = net.IPv6zero + header.TransportProtocol = proxyproto.TCPv6 + header.DestinationAddr = &net.TCPAddr{ + IP: net.IPv6zero, + } default: return nil, fmt.Errorf("unexpected remote addr type in proxy protocol info") } - // TODO: We should probably migrate away from net.IP to use netip.Addr, - // but due to the upstream dependency, we can't do that yet. switch h.ProxyProtocol { case "v1": - header := proxyprotocol.HeaderV1{ - SrcIP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), - SrcPort: int(proxyProtocolInfo.AddrPort.Port()), - DestIP: destIP, - DestPort: 0, - } + header.Version = 1 caddyCtx.Logger().Debug("sending proxy protocol header v1", zap.Any("header", header)) - _, err = header.WriteTo(conn) case "v2": - header := proxyprotocol.HeaderV2{ - Command: proxyprotocol.CmdProxy, - Src: &net.TCPAddr{IP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), Port: int(proxyProtocolInfo.AddrPort.Port())}, - Dest: &net.TCPAddr{IP: destIP, Port: 0}, - } + header.Version = 2 caddyCtx.Logger().Debug("sending proxy protocol header v2", zap.Any("header", header)) - _, err = header.WriteTo(conn) default: return nil, fmt.Errorf("unexpected proxy protocol version") } - + _, err = header.WriteTo(conn) if err != nil { // identify this error as one that occurred during // dialing, which can be important when trying to