From bc04e748f5cd278214b725a767831213c0445a93 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Wed, 1 Nov 2023 22:07:14 +0300 Subject: [PATCH] add config options for `Deny` CIDR and fallback policy --- .../proxyprotocol/listenerwrapper.go | 53 ++++++++++-- modules/caddyhttp/proxyprotocol/module.go | 14 +++- modules/caddyhttp/proxyprotocol/policy.go | 82 +++++++++++++++++++ 3 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 modules/caddyhttp/proxyprotocol/policy.go diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go index fdb764109e5..69829ee1346 100644 --- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go +++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go @@ -37,18 +37,59 @@ type ListenerWrapper struct { // Allow is an optional list of CIDR ranges to // allow/require PROXY headers from. Allow []string `json:"allow,omitempty"` + allow []*net.IPNet - policies []goproxy.PolicyFunc + // Denby is an optional list of CIDR ranges to + // deny PROXY headers from. + Deny []string `json:"deny,omitempty"` + deny []*net.IPNet + + // 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 { - if len(pp.Allow) > 0 { - allowlist, err := goproxy.LaxWhiteListPolicy(pp.Allow) + for _, cidr := range pp.Allow { + _, ipnet, err := net.ParseCIDR(cidr) if err != nil { return err } - pp.policies = append(pp.policies, allowlist) + pp.allow = append(pp.allow, ipnet) + } + for _, cidr := range pp.Deny { + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + pp.deny = append(pp.deny, ipnet) + } + pp.policy = func(upstream net.Addr) (goproxy.Policy, error) { + ret := pp.FallbackPolicy + host, _, err := net.SplitHostPort(upstream.String()) + if err != nil { + return goproxy.REJECT, err + } + ip := net.ParseIP(host) + if ip == 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 } @@ -59,8 +100,6 @@ func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener { Listener: l, ReadHeaderTimeout: time.Duration(pp.Timeout), } - if len(pp.policies) > 0 { - pl.Policy = pp.policies[0] - } + pl.Policy = pp.policy return pl } diff --git a/modules/caddyhttp/proxyprotocol/module.go b/modules/caddyhttp/proxyprotocol/module.go index 78f89c6507b..c6dc283ea5d 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 00000000000..6dc8beb45dc --- /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")