git.iko.soy/typeable/oauth2-proxy
on master help sign in
Implements --trusted-ip option (#552)
64ae31bIsabelle COWAN-BERGMAN
CHANGELOG.md+1
@@ -93,6 +93,7 @@
93
93
94## Changes since v5.1.1
94## Changes since v5.1.1
95
95
96+- [#552](https://github.com/oauth2-proxy/oauth2-proxy/pull/522) Implements --trusted-ip option to allow clients behind specified IPs or CIDR ranges to bypass authentication (@Izzette)
96- [GHSA-5m6c-jp6f-2vcv](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5m6c-jp6f-2vcv) New OpenRedirect cases have been found (@JoelSpeed)
97- [GHSA-5m6c-jp6f-2vcv](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5m6c-jp6f-2vcv) New OpenRedirect cases have been found (@JoelSpeed)
97- [#639](https://github.com/oauth2-proxy/oauth2-proxy/pull/639) Change how gitlab-group is parsed on options (@linuxgemini)
98- [#639](https://github.com/oauth2-proxy/oauth2-proxy/pull/639) Change how gitlab-group is parsed on options (@linuxgemini)
98- [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev)
99- [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev)
contrib/oauth2-proxy_autocomplete.sh+1−1
@@ -24,7 +24,7 @@ _oauth2_proxy() {
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
24 COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
25 return 0
25 return 0
26 ;;
26 ;;
27− --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
27+ --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url))
28 return 0
28 return 0
29 ;;
29 ;;
30 esac
30 esac
docs/configuration/configuration.md+1
@@ -129,6 +129,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
129| `--validate-url` | string | Access token validation endpoint | |
129| `--validate-url` | string | Access token validation endpoint | |
130| `--version` | n/a | print version string | |
130| `--version` | n/a | print version string | |
131| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
131| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
132+| `--trusted-ip` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in a HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
132
133
133Note: when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
134Note: when using the `whitelist-domain` option, any domain prefixed with a `.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URL's protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`.
134
135
oauthproxy.go+32−1
@@ -117,6 +117,7 @@ type OAuthProxy struct {
117 compiledRegex []*regexp.Regexp
117 compiledRegex []*regexp.Regexp
118 templates *template.Template
118 templates *template.Template
119 realClientIPParser ipapi.RealClientIPParser
119 realClientIPParser ipapi.RealClientIPParser
120+ trustedIPs *ip.NetSet
120 Banner string
121 Banner string
121 Footer string
122 Footer string
122}
123}
@@ -303,6 +304,15 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
303
304
304 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
305 logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh)
305
306
307+ trustedIPs := ip.NewNetSet()
308+ for _, ipStr := range opts.TrustedIPs {
309+ if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
310+ trustedIPs.AddIPNet(*ipNet)
311+ } else {
312+ return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
313+ }
314+ }
315+
306 return &OAuthProxy{
316 return &OAuthProxy{
307 CookieName: opts.Cookie.Name,
317 CookieName: opts.Cookie.Name,
308 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
318 CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
@@ -349,6 +359,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
349 PreferEmailToUser: opts.PreferEmailToUser,
359 PreferEmailToUser: opts.PreferEmailToUser,
350 SkipProviderButton: opts.SkipProviderButton,
360 SkipProviderButton: opts.SkipProviderButton,
351 templates: loadTemplates(opts.CustomTemplatesDir),
361 templates: loadTemplates(opts.CustomTemplatesDir),
362+ trustedIPs: trustedIPs,
352 Banner: opts.Banner,
363 Banner: opts.Banner,
353 Footer: opts.Footer,
364 Footer: opts.Footer,
354 }, nil
365 }, nil
@@ -650,7 +661,7 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
650// IsWhitelistedRequest is used to check if auth should be skipped for this request
661// IsWhitelistedRequest is used to check if auth should be skipped for this request
651func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
662func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
652 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
663 isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
653− return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
664+ return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
654}
665}
655
666
656// IsWhitelistedPath is used to check if the request path is allowed without auth
667// IsWhitelistedPath is used to check if the request path is allowed without auth
@@ -678,6 +689,26 @@ func prepareNoCache(w http.ResponseWriter) {
678 }
689 }
679}
690}
680
691
692+// IsTrustedIP is used to check if a request comes from a trusted client IP address.
693+func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
694+ if p.trustedIPs == nil {
695+ return false
696+ }
697+
698+ remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
699+ if err != nil {
700+ logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
701+ // Possibly spoofed X-Real-IP header
702+ return false
703+ }
704+
705+ if remoteAddr == nil {
706+ return false
707+ }
708+
709+ return p.trustedIPs.Has(remoteAddr)
710+}
711+
681func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
712func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
682 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
713 if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
683 prepareNoCache(rw)
714 prepareNoCache(rw)
oauthproxy_test.go+163
@@ -1892,3 +1892,166 @@ func baseTestOptions() *options.Options {
1892 opts.EmailDomains = []string{"*"}
1892 opts.EmailDomains = []string{"*"}
1893 return opts
1893 return opts
1894}
1894}
1895+
1896+func TestTrustedIPs(t *testing.T) {
1897+ tests := []struct {
1898+ name string
1899+ trustedIPs []string
1900+ reverseProxy bool
1901+ realClientIPHeader string
1902+ req *http.Request
1903+ expectTrusted bool
1904+ }{
1905+ // Check unconfigured behavior.
1906+ {
1907+ name: "Default",
1908+ trustedIPs: nil,
1909+ reverseProxy: false,
1910+ realClientIPHeader: "X-Real-IP", // Default value
1911+ req: func() *http.Request {
1912+ req, _ := http.NewRequest("GET", "/", nil)
1913+ return req
1914+ }(),
1915+ expectTrusted: false,
1916+ },
1917+ // Check using req.RemoteAddr (Options.ReverseProxy == false).
1918+ {
1919+ name: "WithRemoteAddr",
1920+ trustedIPs: []string{"127.0.0.1"},
1921+ reverseProxy: false,
1922+ realClientIPHeader: "X-Real-IP", // Default value
1923+ req: func() *http.Request {
1924+ req, _ := http.NewRequest("GET", "/", nil)
1925+ req.RemoteAddr = "127.0.0.1:43670"
1926+ return req
1927+ }(),
1928+ expectTrusted: true,
1929+ },
1930+ // Check ignores req.RemoteAddr match when behind a reverse proxy / missing header.
1931+ {
1932+ name: "IgnoresRemoteAddrInReverseProxyMode",
1933+ trustedIPs: []string{"127.0.0.1"},
1934+ reverseProxy: true,
1935+ realClientIPHeader: "X-Real-IP", // Default value
1936+ req: func() *http.Request {
1937+ req, _ := http.NewRequest("GET", "/", nil)
1938+ req.RemoteAddr = "127.0.0.1:44324"
1939+ return req
1940+ }(),
1941+ expectTrusted: false,
1942+ },
1943+ // Check successful trusting of localhost in IPv4.
1944+ {
1945+ name: "TrustsLocalhostInReverseProxyMode",
1946+ trustedIPs: []string{"127.0.0.0/8", "::1"},
1947+ reverseProxy: true,
1948+ realClientIPHeader: "X-Forwarded-For",
1949+ req: func() *http.Request {
1950+ req, _ := http.NewRequest("GET", "/", nil)
1951+ req.Header.Add("X-Forwarded-For", "127.0.0.1")
1952+ return req
1953+ }(),
1954+ expectTrusted: true,
1955+ },
1956+ // Check successful trusting of localhost in IPv6.
1957+ {
1958+ name: "TrustsIP6LocalostInReverseProxyMode",
1959+ trustedIPs: []string{"127.0.0.0/8", "::1"},
1960+ reverseProxy: true,
1961+ realClientIPHeader: "X-Forwarded-For",
1962+ req: func() *http.Request {
1963+ req, _ := http.NewRequest("GET", "/", nil)
1964+ req.Header.Add("X-Forwarded-For", "::1")
1965+ return req
1966+ }(),
1967+ expectTrusted: true,
1968+ },
1969+ // Check does not trust random IPv4 address.
1970+ {
1971+ name: "DoesNotTrustRandomIP4Address",
1972+ trustedIPs: []string{"127.0.0.0/8", "::1"},
1973+ reverseProxy: true,
1974+ realClientIPHeader: "X-Forwarded-For",
1975+ req: func() *http.Request {
1976+ req, _ := http.NewRequest("GET", "/", nil)
1977+ req.Header.Add("X-Forwarded-For", "12.34.56.78")
1978+ return req
1979+ }(),
1980+ expectTrusted: false,
1981+ },
1982+ // Check does not trust random IPv6 address.
1983+ {
1984+ name: "DoesNotTrustRandomIP6Address",
1985+ trustedIPs: []string{"127.0.0.0/8", "::1"},
1986+ reverseProxy: true,
1987+ realClientIPHeader: "X-Forwarded-For",
1988+ req: func() *http.Request {
1989+ req, _ := http.NewRequest("GET", "/", nil)
1990+ req.Header.Add("X-Forwarded-For", "::2")
1991+ return req
1992+ }(),
1993+ expectTrusted: false,
1994+ },
1995+ // Check respects correct header.
1996+ {
1997+ name: "RespectsCorrectHeaderInReverseProxyMode",
1998+ trustedIPs: []string{"127.0.0.0/8", "::1"},
1999+ reverseProxy: true,
2000+ realClientIPHeader: "X-Forwarded-For",
2001+ req: func() *http.Request {
2002+ req, _ := http.NewRequest("GET", "/", nil)
2003+ req.Header.Add("X-Real-IP", "::1")
2004+ return req
2005+ }(),
2006+ expectTrusted: false,
2007+ },
2008+ // Check doesn't trust if garbage is provided.
2009+ {
2010+ name: "DoesNotTrustGarbageInReverseProxyMode",
2011+ trustedIPs: []string{"127.0.0.0/8", "::1"},
2012+ reverseProxy: true,
2013+ realClientIPHeader: "X-Forwarded-For",
2014+ req: func() *http.Request {
2015+ req, _ := http.NewRequest("GET", "/", nil)
2016+ req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
2017+ return req
2018+ }(),
2019+ expectTrusted: false,
2020+ },
2021+ // Check doesn't trust if garbage is provided (no reverse-proxy).
2022+ {
2023+ name: "DoesNotTrustGarbage",
2024+ trustedIPs: []string{"127.0.0.0/8", "::1"},
2025+ reverseProxy: false,
2026+ realClientIPHeader: "X-Real-IP",
2027+ req: func() *http.Request {
2028+ req, _ := http.NewRequest("GET", "/", nil)
2029+ req.RemoteAddr = "adsfljk29242as!!"
2030+ return req
2031+ }(),
2032+ expectTrusted: false,
2033+ },
2034+ }
2035+
2036+ for _, tt := range tests {
2037+ t.Run(tt.name, func(t *testing.T) {
2038+ opts := baseTestOptions()
2039+ opts.Upstreams = []string{"static://200"}
2040+ opts.TrustedIPs = tt.trustedIPs
2041+ opts.ReverseProxy = tt.reverseProxy
2042+ opts.RealClientIPHeader = tt.realClientIPHeader
2043+ validation.Validate(opts)
2044+
2045+ proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
2046+ assert.NoError(t, err)
2047+ rw := httptest.NewRecorder()
2048+
2049+ proxy.ServeHTTP(rw, tt.req)
2050+ if tt.expectTrusted {
2051+ assert.Equal(t, 200, rw.Code)
2052+ } else {
2053+ assert.Equal(t, 403, rw.Code)
2054+ }
2055+ })
2056+ }
2057+}
pkg/apis/options/options.go+17−15
@@ -21,21 +21,22 @@ type SignatureData struct {
21// Options holds Configuration Options that can be set by Command Line Flag,
21// Options holds Configuration Options that can be set by Command Line Flag,
22// or Config File
22// or Config File
23type Options struct {
23type Options struct {
24− ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
24+ ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
25− PingPath string `flag:"ping-path" cfg:"ping_path"`
25+ PingPath string `flag:"ping-path" cfg:"ping_path"`
26− PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
26+ PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
27− ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
27+ ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
28− HTTPAddress string `flag:"http-address" cfg:"http_address"`
28+ HTTPAddress string `flag:"http-address" cfg:"http_address"`
29− HTTPSAddress string `flag:"https-address" cfg:"https_address"`
29+ HTTPSAddress string `flag:"https-address" cfg:"https_address"`
30− ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
30+ ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
31− RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
31+ RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
32+ TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
32− ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
33+ ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
33− RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
34+ RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
34− ClientID string `flag:"client-id" cfg:"client_id"`
35+ ClientID string `flag:"client-id" cfg:"client_id"`
35− ClientSecret string `flag:"client-secret" cfg:"client_secret"`
36+ ClientSecret string `flag:"client-secret" cfg:"client_secret"`
36− ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
37+ ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
37− TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file"`
38+ TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file"`
38− TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
39+ TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
39
40
40 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
41 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
41 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
42 KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
@@ -186,6 +187,7 @@ func NewFlagSet() *pflag.FlagSet {
186 flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
187 flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
187 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
188 flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
188 flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)")
189 flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)")
190+ flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.")
189 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
191 flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
190 flagSet.String("tls-cert-file", "", "path to certificate file")
192 flagSet.String("tls-cert-file", "", "path to certificate file")
191 flagSet.String("tls-key-file", "", "path to private key file")
193 flagSet.String("tls-key-file", "", "path to private key file")
pkg/ip/net_set.gonew file+112
@@ -0,0 +1,112 @@
1+package ip
2+
3+import (
4+ "fmt"
5+ "net"
6+)
7+
8+// Fast lookup table for intersection of a single IP address within a collection of CIDR networks.
9+//
10+// Supports 4-byte (IPv4) and 16-byte (IPv6) networks.
11+//
12+// Provides O(1) best-case, O(log(n)) worst-case performance.
13+// In practice netmasks included will generally only be of standard lengths:
14+// - /8, /16, /24, and /32 for IPv4
15+// - /64 and /128 for IPv6.
16+// As a result, typical lookup times will lean closer to best-case rather than worst-case even when most of the internet
17+// is included.
18+type NetSet struct {
19+ ip4NetMaps []ipNetMap
20+ ip6NetMaps []ipNetMap
21+}
22+
23+// Create a new NetSet with all of the provided networks.
24+func NewNetSet() *NetSet {
25+ return &NetSet{
26+ ip4NetMaps: make([]ipNetMap, 0),
27+ ip6NetMaps: make([]ipNetMap, 0),
28+ }
29+}
30+
31+// Check if `ip` is in the set, true if within the set otherwise false.
32+func (w *NetSet) Has(ip net.IP) bool {
33+ netMaps := w.getNetMaps(ip)
34+
35+ // Check all ipNetMaps for intersection with `ip`.
36+ for _, netMap := range *netMaps {
37+ if netMap.has(ip) {
38+ return true
39+ }
40+ }
41+ return false
42+}
43+
44+// Add an CIDR network to the set.
45+func (w *NetSet) AddIPNet(ipNet net.IPNet) {
46+ netMaps := w.getNetMaps(ipNet.IP)
47+
48+ // Determine the size / number of ones in the CIDR network mask.
49+ ones, _ := ipNet.Mask.Size()
50+
51+ var netMap *ipNetMap
52+
53+ // Search for the ipNetMap containing networks with the same number of ones.
54+ for i := 0; len(*netMaps) > i; i++ {
55+ if netMapOnes, _ := (*netMaps)[i].mask.Size(); netMapOnes == ones {
56+ netMap = &(*netMaps)[i]
57+ break
58+ }
59+ }
60+
61+ // Create a new ipNetMap if none with this number of ones have been created yet.
62+ if netMap == nil {
63+ netMap = &ipNetMap{
64+ mask: ipNet.Mask,
65+ ips: make(map[string]bool),
66+ }
67+ *netMaps = append(*netMaps, *netMap)
68+ // Recurse once now that there exists an netMap.
69+ w.AddIPNet(ipNet)
70+ return
71+ }
72+
73+ // Add the IP to the ipNetMap.
74+ netMap.ips[ipNet.IP.String()] = true
75+}
76+
77+// Get the appropriate array of networks for the given IP version.
78+func (w *NetSet) getNetMaps(ip net.IP) (netMaps *[]ipNetMap) {
79+ switch {
80+ case ip.To4() != nil:
81+ netMaps = &w.ip4NetMaps
82+ case ip.To16() != nil:
83+ netMaps = &w.ip6NetMaps
84+ default:
85+ panic(fmt.Sprintf("IP (%s) is neither 4-byte nor 16-byte?", ip.String()))
86+ }
87+
88+ return netMaps
89+}
90+
91+// Hash-set of CIDR networks with the same mask size.
92+type ipNetMap struct {
93+ mask net.IPMask
94+ ips map[string]bool
95+}
96+
97+// Check if the IP is in any of the CIDR networks contained in this map.
98+func (m ipNetMap) has(ip net.IP) bool {
99+ // Apply the mask to the IP to remove any irrelevant bits in the IP.
100+ ipMasked := ip.Mask(m.mask)
101+ if ipMasked == nil {
102+ panic(fmt.Sprintf(
103+ "Mismatch in net.IPMask and net.IP protocol version, cannot apply mask %s to %s",
104+ m.mask.String(), ip.String()))
105+ }
106+
107+ // Check if the masked IP is the same as any of the networks.
108+ if _, ok := m.ips[ipMasked.String()]; ok {
109+ return true
110+ }
111+ return false
112+}
pkg/ip/net_set_test.gonew file+117
@@ -0,0 +1,117 @@
1+package ip
2+
3+import (
4+ "net"
5+ "testing"
6+
7+ "github.com/stretchr/testify/assert"
8+)
9+
10+func TestEmptyNetSet(t *testing.T) {
11+ set := NewNetSet()
12+
13+ ips := []string{
14+ "127.0.0.1",
15+ "241.163.75.163",
16+ "189.248.81.89",
17+ "167.75.10.139",
18+ "22.169.8.110",
19+ "::1",
20+ "114d:28ae:60a:7b12:b851:fd49:5ff9:9b9d",
21+ "4e73:55a5:6cd:29c7:5db6:673a:4608:1a3f",
22+ "f6b0:d7f3:8206:b529:ee3b:49ff:1b3b:9aac",
23+ "09d8:a74c:652f:3763:b87d:f068:b593:b588",
24+ }
25+ for _, ip := range ips {
26+ assert.Falsef(t, set.Has(net.ParseIP(ip)), "Empty NetSet must not have %q", ip)
27+ }
28+}
29+
30+func TestV4ContainsEverything(t *testing.T) {
31+ set := NewNetSet()
32+ set.AddIPNet(net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: net.CIDRMask(0, 32)})
33+
34+ ip4s := []string{
35+ "127.0.0.1",
36+ "250.169.183.192",
37+ "175.125.175.160",
38+ "178.244.137.158",
39+ "190.13.12.194",
40+ }
41+ for _, ip := range ip4s {
42+ assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"0.0.0.0/0\"} must have %q", ip)
43+ }
44+ ip6s := []string{
45+ "::1",
46+ "7261:ca92:c9a5:4b79:2e:8889:b769:1d62",
47+ "f54f:10cb:e6a4:89f5:3366:1e3e:2d22:f68e",
48+ "d1f3:be6:60d:361:1717:4fe6:9812:1a6c",
49+ "7eaa:2a8:365c:b55f:67cd:96dd:602a:4385",
50+ }
51+ for _, ip := range ip6s {
52+ assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"0.0.0.0/0\"} must not have %q", ip)
53+ }
54+}
55+
56+func TestV6ContainsEverything(t *testing.T) {
57+ set := NewNetSet()
58+ set.AddIPNet(net.IPNet{IP: net.ParseIP("::"), Mask: net.CIDRMask(0, 128)})
59+
60+ ip4s := []string{
61+ "127.0.0.1",
62+ "216.163.140.97",
63+ "175.55.31.226",
64+ "115.112.186.19",
65+ "250.83.241.122",
66+ }
67+ for _, ip := range ip4s {
68+ assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"::/0\"} must not have %q", ip)
69+ }
70+ ip6s := []string{
71+ "::1",
72+ "fa3b:7f17:7521:d9f4:d855:51f9:4b63:de7f",
73+ "4d47:c29:2479:41ce:69f9:3d33:306a:91c8",
74+ "fe99:f364:f8cd:7a11:838:a36f:b9a1:965",
75+ "fb9e:d809:881d:9cee:533d:1ba5:592e:ea9b",
76+ }
77+ for _, ip := range ip6s {
78+ assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"::/0\"} must have %q", ip)
79+ }
80+}
81+
82+func TestLocalhostOnly(t *testing.T) {
83+ set := NewNetSet()
84+ set.AddIPNet(net.IPNet{IP: net.ParseIP("127.0.0.0"), Mask: net.CIDRMask(8, 32)})
85+ set.AddIPNet(net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)})
86+
87+ included := []string{
88+ "127.0.0.1",
89+ "127.196.199.162",
90+ "127.78.85.40",
91+ "127.205.171.240",
92+ "127.121.187.93",
93+ "127.5.217.10",
94+ "127.22.172.251",
95+ "127.12.60.198",
96+ "127.110.240.252",
97+ "::1",
98+ }
99+ for _, ip := range included {
100+ assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"127.0.0.0/8\", \"::1\"} must have %q", ip)
101+ }
102+ excluded := []string{
103+ "15.65.169.230",
104+ "196.72.207.65",
105+ "43.46.246.121",
106+ "69.147.193.224",
107+ "43.130.74.115",
108+ "8375:b8f9:70bf:a247:f55e:b0c0:c24f:eebe",
109+ "d325:aabf:ef49:2d46:ba47:1aa5:9e57:b0bf",
110+ "d2f6:831:170:e04e:bd6d:7ae7:e14a:71c3",
111+ "ca80:483f:bb7f:df2a:bceb:de5e:6129:9625",
112+ "f3fe:f771:9138:5d63:8e83:7033:2e6f:4762",
113+ }
114+ for _, ip := range excluded {
115+ assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"127.0.0.0/8\", \"::1\"} must not have %q", ip)
116+ }
117+}
pkg/ip/parse_ip_net.gonew file+39
@@ -0,0 +1,39 @@
1+package ip
2+
3+import (
4+ "net"
5+ "strings"
6+)
7+
8+func ParseIPNet(s string) *net.IPNet {
9+ if !strings.ContainsRune(s, '/') {
10+ ip := net.ParseIP(s)
11+ if ip == nil {
12+ return nil
13+ }
14+
15+ var mask net.IPMask
16+ switch {
17+ case ip.To4() != nil:
18+ mask = net.CIDRMask(32, 32)
19+ case ip.To16() != nil:
20+ mask = net.CIDRMask(128, 128)
21+ default:
22+ return nil
23+ }
24+
25+ return &net.IPNet{
26+ IP: ip,
27+ Mask: mask,
28+ }
29+ }
30+
31+ switch ip, ipNet, err := net.ParseCIDR(s); {
32+ case err != nil:
33+ return nil
34+ case !ipNet.IP.Equal(ip):
35+ return nil
36+ default:
37+ return ipNet
38+ }
39+}
pkg/ip/realclientip.go+8
@@ -58,6 +58,14 @@ func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, err
58 return ip, nil
58 return ip, nil
59}
59}
60
60
61+// GetClientIP obtains the perceived end-user IP address from headers if p != nil else from req.RemoteAddr.
62+func GetClientIP(p ipapi.RealClientIPParser, req *http.Request) (net.IP, error) {
63+ if p != nil {
64+ return p.GetRealClientIP(req.Header)
65+ }
66+ return getRemoteIP(req)
67+}
68+
61// getRemoteIP obtains the IP of the low-level connected network host
69// getRemoteIP obtains the IP of the low-level connected network host
62func getRemoteIP(req *http.Request) (net.IP, error) {
70func getRemoteIP(req *http.Request) (net.IP, error) {
63 if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
71 if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
pkg/validation/options.go+10
@@ -225,6 +225,16 @@ func Validate(o *options.Options) error {
225 })
225 })
226 }
226 }
227
227
228+ if len(o.TrustedIPs) > 0 && o.ReverseProxy {
229+ fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
230+ }
231+
232+ for i, ipStr := range o.TrustedIPs {
233+ if nil == ip.ParseIPNet(ipStr) {
234+ msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr))
235+ }
236+ }
237+
228 if len(msgs) != 0 {
238 if len(msgs) != 0 {
229 return fmt.Errorf("invalid configuration:\n %s",
239 return fmt.Errorf("invalid configuration:\n %s",
230 strings.Join(msgs, "\n "))
240 strings.Join(msgs, "\n "))
pkg/validation/options_test.go+40
@@ -2,6 +2,7 @@ package validation
2
2
3import (
3import (
4 "crypto"
4 "crypto"
5+ "errors"
5 "io/ioutil"
6 "io/ioutil"
6 "net/url"
7 "net/url"
7 "os"
8 "os"
@@ -352,6 +353,45 @@ func TestRealClientIPHeader(t *testing.T) {
352 assert.Nil(t, o.GetRealClientIPParser())
353 assert.Nil(t, o.GetRealClientIPParser())
353}
354}
354
355
356+func TestIPCIDRSetOption(t *testing.T) {
357+ tests := []struct {
358+ name string
359+ trustedIPs []string
360+ err error
361+ }{
362+ {
363+ "TestSomeIPs",
364+ []string{"127.0.0.1", "10.32.0.1/32", "43.36.201.0/24", "::1", "2a12:105:ee7:9234:0:0:0:0/64"},
365+ nil,
366+ }, {
367+ "TestOverlappingIPs",
368+ []string{"135.180.78.199", "135.180.78.199/32", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128"},
369+ nil,
370+ }, {
371+ "TestInvalidIPs",
372+ []string{"[::1]", "alkwlkbn/32"},
373+ errors.New(
374+ "invalid configuration:\n" +
375+ " trusted_ips[0] ([::1]) could not be recognized\n" +
376+ " trusted_ips[1] (alkwlkbn/32) could not be recognized",
377+ ),
378+ },
379+ }
380+
381+ for _, tt := range tests {
382+ t.Run(tt.name, func(t *testing.T) {
383+ o := testOptions()
384+ o.TrustedIPs = tt.trustedIPs
385+ err := Validate(o)
386+ if tt.err == nil {
387+ assert.Nil(t, err)
388+ } else {
389+ assert.Equal(t, tt.err.Error(), err.Error())
390+ }
391+ })
392+ }
393+}
394+
355func TestProviderCAFilesError(t *testing.T) {
395func TestProviderCAFilesError(t *testing.T) {
356 file, err := ioutil.TempFile("", "absent.*.crt")
396 file, err := ioutil.TempFile("", "absent.*.crt")
357 assert.NoError(t, err)
397 assert.NoError(t, err)