mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2024-11-28 09:08:44 +02:00
Implements --trusted-ip option (#552)
* Implements --ip-whitelist option * Included IPWhitelist option to allow one-or-more selected CIDR ranges to bypass OAuth2 authentication. * Adds IPWhitelist, a fast lookup table for multiple CIDR ranges. * Renamed IPWhitelist ipCIDRSet * Fixed unessesary pointer usage in ipCIDRSet * Update CHANGELOG.md * Update CHANGELOG.md * Updated to not use err.Error() in printf statements * Imrpoved language for --ip-whitelist descriptions. * Improve IP whitelist options error messages * Clarify options single-host normalization * Wrote a book about ipCIDRSet * Added comment to IsWhitelistedIP in oauthproxy.go * Rewrite oauthproxy test case as table driven * oops * Support whitelisting by low-level remote address * Added more test-cases, improved descriptions * Move ip_cidr_set.go to pkg/ip/net_set.go * Add more whitelist test use cases. * Oops * Use subtests for TestIPWhitelist * Add minimal tests for ip.NetSet * Use switch statment * Renamed ip-whitelist to whitelist-ip * Update documentation with a warning. * Update pkg/apis/options/options.go * Update CHANGELOG.md Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Update pkg/ip/net_set_test.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix fmt * Move ParseIPNet into abstraction * Add warning in case of --reverse-proxy * Update pkg/validation/options_test.go * Rename --whitelist-ip to --trusted-ip * Update oauthproxy.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * fix Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
This commit is contained in:
parent
e6903d8c1f
commit
64ae31b5a0
@ -93,6 +93,7 @@
|
||||
|
||||
## Changes since v5.1.1
|
||||
|
||||
- [#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)
|
||||
- [GHSA-5m6c-jp6f-2vcv](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-5m6c-jp6f-2vcv) New OpenRedirect cases have been found (@JoelSpeed)
|
||||
- [#639](https://github.com/oauth2-proxy/oauth2-proxy/pull/639) Change how gitlab-group is parsed on options (@linuxgemini)
|
||||
- [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev)
|
||||
|
@ -24,7 +24,7 @@ _oauth2_proxy() {
|
||||
COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--@(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))
|
||||
--@(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))
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
@ -129,6 +129,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example
|
||||
| `--validate-url` | string | Access token validation endpoint | |
|
||||
| `--version` | n/a | print version string | |
|
||||
| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` to allow subdomains (eg `.example.com`) | |
|
||||
| `--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. | |
|
||||
|
||||
Note: 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:*`.
|
||||
|
||||
|
@ -117,6 +117,7 @@ type OAuthProxy struct {
|
||||
compiledRegex []*regexp.Regexp
|
||||
templates *template.Template
|
||||
realClientIPParser ipapi.RealClientIPParser
|
||||
trustedIPs *ip.NetSet
|
||||
Banner string
|
||||
Footer string
|
||||
}
|
||||
@ -303,6 +304,15 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||
|
||||
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)
|
||||
|
||||
trustedIPs := ip.NewNetSet()
|
||||
for _, ipStr := range opts.TrustedIPs {
|
||||
if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
|
||||
trustedIPs.AddIPNet(*ipNet)
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
|
||||
}
|
||||
}
|
||||
|
||||
return &OAuthProxy{
|
||||
CookieName: opts.Cookie.Name,
|
||||
CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
|
||||
@ -349,6 +359,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
|
||||
PreferEmailToUser: opts.PreferEmailToUser,
|
||||
SkipProviderButton: opts.SkipProviderButton,
|
||||
templates: loadTemplates(opts.CustomTemplatesDir),
|
||||
trustedIPs: trustedIPs,
|
||||
Banner: opts.Banner,
|
||||
Footer: opts.Footer,
|
||||
}, nil
|
||||
@ -650,7 +661,7 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
|
||||
// IsWhitelistedRequest is used to check if auth should be skipped for this request
|
||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
|
||||
isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
|
||||
return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
|
||||
return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
|
||||
}
|
||||
|
||||
// IsWhitelistedPath is used to check if the request path is allowed without auth
|
||||
@ -678,6 +689,26 @@ func prepareNoCache(w http.ResponseWriter) {
|
||||
}
|
||||
}
|
||||
|
||||
// IsTrustedIP is used to check if a request comes from a trusted client IP address.
|
||||
func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
|
||||
if p.trustedIPs == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
|
||||
if err != nil {
|
||||
logger.Printf("Error obtaining real IP for trusted IP list: %v", err)
|
||||
// Possibly spoofed X-Real-IP header
|
||||
return false
|
||||
}
|
||||
|
||||
if remoteAddr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.trustedIPs.Has(remoteAddr)
|
||||
}
|
||||
|
||||
func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
|
||||
prepareNoCache(rw)
|
||||
|
@ -1892,3 +1892,166 @@ func baseTestOptions() *options.Options {
|
||||
opts.EmailDomains = []string{"*"}
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestTrustedIPs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trustedIPs []string
|
||||
reverseProxy bool
|
||||
realClientIPHeader string
|
||||
req *http.Request
|
||||
expectTrusted bool
|
||||
}{
|
||||
// Check unconfigured behavior.
|
||||
{
|
||||
name: "Default",
|
||||
trustedIPs: nil,
|
||||
reverseProxy: false,
|
||||
realClientIPHeader: "X-Real-IP", // Default value
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check using req.RemoteAddr (Options.ReverseProxy == false).
|
||||
{
|
||||
name: "WithRemoteAddr",
|
||||
trustedIPs: []string{"127.0.0.1"},
|
||||
reverseProxy: false,
|
||||
realClientIPHeader: "X-Real-IP", // Default value
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:43670"
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: true,
|
||||
},
|
||||
// Check ignores req.RemoteAddr match when behind a reverse proxy / missing header.
|
||||
{
|
||||
name: "IgnoresRemoteAddrInReverseProxyMode",
|
||||
trustedIPs: []string{"127.0.0.1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Real-IP", // Default value
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:44324"
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check successful trusting of localhost in IPv4.
|
||||
{
|
||||
name: "TrustsLocalhostInReverseProxyMode",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Forwarded-For", "127.0.0.1")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: true,
|
||||
},
|
||||
// Check successful trusting of localhost in IPv6.
|
||||
{
|
||||
name: "TrustsIP6LocalostInReverseProxyMode",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Forwarded-For", "::1")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: true,
|
||||
},
|
||||
// Check does not trust random IPv4 address.
|
||||
{
|
||||
name: "DoesNotTrustRandomIP4Address",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Forwarded-For", "12.34.56.78")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check does not trust random IPv6 address.
|
||||
{
|
||||
name: "DoesNotTrustRandomIP6Address",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Forwarded-For", "::2")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check respects correct header.
|
||||
{
|
||||
name: "RespectsCorrectHeaderInReverseProxyMode",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Real-IP", "::1")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check doesn't trust if garbage is provided.
|
||||
{
|
||||
name: "DoesNotTrustGarbageInReverseProxyMode",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: true,
|
||||
realClientIPHeader: "X-Forwarded-For",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.Header.Add("X-Forwarded-For", "adsfljk29242as!!")
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
// Check doesn't trust if garbage is provided (no reverse-proxy).
|
||||
{
|
||||
name: "DoesNotTrustGarbage",
|
||||
trustedIPs: []string{"127.0.0.0/8", "::1"},
|
||||
reverseProxy: false,
|
||||
realClientIPHeader: "X-Real-IP",
|
||||
req: func() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "adsfljk29242as!!"
|
||||
return req
|
||||
}(),
|
||||
expectTrusted: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := baseTestOptions()
|
||||
opts.Upstreams = []string{"static://200"}
|
||||
opts.TrustedIPs = tt.trustedIPs
|
||||
opts.ReverseProxy = tt.reverseProxy
|
||||
opts.RealClientIPHeader = tt.realClientIPHeader
|
||||
validation.Validate(opts)
|
||||
|
||||
proxy, err := NewOAuthProxy(opts, func(string) bool { return true })
|
||||
assert.NoError(t, err)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
proxy.ServeHTTP(rw, tt.req)
|
||||
if tt.expectTrusted {
|
||||
assert.Equal(t, 200, rw.Code)
|
||||
} else {
|
||||
assert.Equal(t, 403, rw.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -21,21 +21,22 @@ type SignatureData struct {
|
||||
// Options holds Configuration Options that can be set by Command Line Flag,
|
||||
// or Config File
|
||||
type Options struct {
|
||||
ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
|
||||
PingPath string `flag:"ping-path" cfg:"ping_path"`
|
||||
PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
|
||||
ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
|
||||
HTTPAddress string `flag:"http-address" cfg:"http_address"`
|
||||
HTTPSAddress string `flag:"https-address" cfg:"https_address"`
|
||||
ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
|
||||
RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
|
||||
ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
|
||||
RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
|
||||
ClientID string `flag:"client-id" cfg:"client_id"`
|
||||
ClientSecret string `flag:"client-secret" cfg:"client_secret"`
|
||||
ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
|
||||
TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file"`
|
||||
TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
|
||||
ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"`
|
||||
PingPath string `flag:"ping-path" cfg:"ping_path"`
|
||||
PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"`
|
||||
ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
|
||||
HTTPAddress string `flag:"http-address" cfg:"http_address"`
|
||||
HTTPSAddress string `flag:"https-address" cfg:"https_address"`
|
||||
ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"`
|
||||
RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"`
|
||||
TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"`
|
||||
ForceHTTPS bool `flag:"force-https" cfg:"force_https"`
|
||||
RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
|
||||
ClientID string `flag:"client-id" cfg:"client_id"`
|
||||
ClientSecret string `flag:"client-secret" cfg:"client_secret"`
|
||||
ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"`
|
||||
TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file"`
|
||||
TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"`
|
||||
|
||||
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
|
||||
KeycloakGroup string `flag:"keycloak-group" cfg:"keycloak_group"`
|
||||
@ -186,6 +187,7 @@ func NewFlagSet() *pflag.FlagSet {
|
||||
flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
|
||||
flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted")
|
||||
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)")
|
||||
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.")
|
||||
flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests")
|
||||
flagSet.String("tls-cert-file", "", "path to certificate file")
|
||||
flagSet.String("tls-key-file", "", "path to private key file")
|
||||
|
112
pkg/ip/net_set.go
Normal file
112
pkg/ip/net_set.go
Normal file
@ -0,0 +1,112 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Fast lookup table for intersection of a single IP address within a collection of CIDR networks.
|
||||
//
|
||||
// Supports 4-byte (IPv4) and 16-byte (IPv6) networks.
|
||||
//
|
||||
// Provides O(1) best-case, O(log(n)) worst-case performance.
|
||||
// In practice netmasks included will generally only be of standard lengths:
|
||||
// - /8, /16, /24, and /32 for IPv4
|
||||
// - /64 and /128 for IPv6.
|
||||
// As a result, typical lookup times will lean closer to best-case rather than worst-case even when most of the internet
|
||||
// is included.
|
||||
type NetSet struct {
|
||||
ip4NetMaps []ipNetMap
|
||||
ip6NetMaps []ipNetMap
|
||||
}
|
||||
|
||||
// Create a new NetSet with all of the provided networks.
|
||||
func NewNetSet() *NetSet {
|
||||
return &NetSet{
|
||||
ip4NetMaps: make([]ipNetMap, 0),
|
||||
ip6NetMaps: make([]ipNetMap, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if `ip` is in the set, true if within the set otherwise false.
|
||||
func (w *NetSet) Has(ip net.IP) bool {
|
||||
netMaps := w.getNetMaps(ip)
|
||||
|
||||
// Check all ipNetMaps for intersection with `ip`.
|
||||
for _, netMap := range *netMaps {
|
||||
if netMap.has(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add an CIDR network to the set.
|
||||
func (w *NetSet) AddIPNet(ipNet net.IPNet) {
|
||||
netMaps := w.getNetMaps(ipNet.IP)
|
||||
|
||||
// Determine the size / number of ones in the CIDR network mask.
|
||||
ones, _ := ipNet.Mask.Size()
|
||||
|
||||
var netMap *ipNetMap
|
||||
|
||||
// Search for the ipNetMap containing networks with the same number of ones.
|
||||
for i := 0; len(*netMaps) > i; i++ {
|
||||
if netMapOnes, _ := (*netMaps)[i].mask.Size(); netMapOnes == ones {
|
||||
netMap = &(*netMaps)[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new ipNetMap if none with this number of ones have been created yet.
|
||||
if netMap == nil {
|
||||
netMap = &ipNetMap{
|
||||
mask: ipNet.Mask,
|
||||
ips: make(map[string]bool),
|
||||
}
|
||||
*netMaps = append(*netMaps, *netMap)
|
||||
// Recurse once now that there exists an netMap.
|
||||
w.AddIPNet(ipNet)
|
||||
return
|
||||
}
|
||||
|
||||
// Add the IP to the ipNetMap.
|
||||
netMap.ips[ipNet.IP.String()] = true
|
||||
}
|
||||
|
||||
// Get the appropriate array of networks for the given IP version.
|
||||
func (w *NetSet) getNetMaps(ip net.IP) (netMaps *[]ipNetMap) {
|
||||
switch {
|
||||
case ip.To4() != nil:
|
||||
netMaps = &w.ip4NetMaps
|
||||
case ip.To16() != nil:
|
||||
netMaps = &w.ip6NetMaps
|
||||
default:
|
||||
panic(fmt.Sprintf("IP (%s) is neither 4-byte nor 16-byte?", ip.String()))
|
||||
}
|
||||
|
||||
return netMaps
|
||||
}
|
||||
|
||||
// Hash-set of CIDR networks with the same mask size.
|
||||
type ipNetMap struct {
|
||||
mask net.IPMask
|
||||
ips map[string]bool
|
||||
}
|
||||
|
||||
// Check if the IP is in any of the CIDR networks contained in this map.
|
||||
func (m ipNetMap) has(ip net.IP) bool {
|
||||
// Apply the mask to the IP to remove any irrelevant bits in the IP.
|
||||
ipMasked := ip.Mask(m.mask)
|
||||
if ipMasked == nil {
|
||||
panic(fmt.Sprintf(
|
||||
"Mismatch in net.IPMask and net.IP protocol version, cannot apply mask %s to %s",
|
||||
m.mask.String(), ip.String()))
|
||||
}
|
||||
|
||||
// Check if the masked IP is the same as any of the networks.
|
||||
if _, ok := m.ips[ipMasked.String()]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
117
pkg/ip/net_set_test.go
Normal file
117
pkg/ip/net_set_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEmptyNetSet(t *testing.T) {
|
||||
set := NewNetSet()
|
||||
|
||||
ips := []string{
|
||||
"127.0.0.1",
|
||||
"241.163.75.163",
|
||||
"189.248.81.89",
|
||||
"167.75.10.139",
|
||||
"22.169.8.110",
|
||||
"::1",
|
||||
"114d:28ae:60a:7b12:b851:fd49:5ff9:9b9d",
|
||||
"4e73:55a5:6cd:29c7:5db6:673a:4608:1a3f",
|
||||
"f6b0:d7f3:8206:b529:ee3b:49ff:1b3b:9aac",
|
||||
"09d8:a74c:652f:3763:b87d:f068:b593:b588",
|
||||
}
|
||||
for _, ip := range ips {
|
||||
assert.Falsef(t, set.Has(net.ParseIP(ip)), "Empty NetSet must not have %q", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV4ContainsEverything(t *testing.T) {
|
||||
set := NewNetSet()
|
||||
set.AddIPNet(net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: net.CIDRMask(0, 32)})
|
||||
|
||||
ip4s := []string{
|
||||
"127.0.0.1",
|
||||
"250.169.183.192",
|
||||
"175.125.175.160",
|
||||
"178.244.137.158",
|
||||
"190.13.12.194",
|
||||
}
|
||||
for _, ip := range ip4s {
|
||||
assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"0.0.0.0/0\"} must have %q", ip)
|
||||
}
|
||||
ip6s := []string{
|
||||
"::1",
|
||||
"7261:ca92:c9a5:4b79:2e:8889:b769:1d62",
|
||||
"f54f:10cb:e6a4:89f5:3366:1e3e:2d22:f68e",
|
||||
"d1f3:be6:60d:361:1717:4fe6:9812:1a6c",
|
||||
"7eaa:2a8:365c:b55f:67cd:96dd:602a:4385",
|
||||
}
|
||||
for _, ip := range ip6s {
|
||||
assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"0.0.0.0/0\"} must not have %q", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV6ContainsEverything(t *testing.T) {
|
||||
set := NewNetSet()
|
||||
set.AddIPNet(net.IPNet{IP: net.ParseIP("::"), Mask: net.CIDRMask(0, 128)})
|
||||
|
||||
ip4s := []string{
|
||||
"127.0.0.1",
|
||||
"216.163.140.97",
|
||||
"175.55.31.226",
|
||||
"115.112.186.19",
|
||||
"250.83.241.122",
|
||||
}
|
||||
for _, ip := range ip4s {
|
||||
assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"::/0\"} must not have %q", ip)
|
||||
}
|
||||
ip6s := []string{
|
||||
"::1",
|
||||
"fa3b:7f17:7521:d9f4:d855:51f9:4b63:de7f",
|
||||
"4d47:c29:2479:41ce:69f9:3d33:306a:91c8",
|
||||
"fe99:f364:f8cd:7a11:838:a36f:b9a1:965",
|
||||
"fb9e:d809:881d:9cee:533d:1ba5:592e:ea9b",
|
||||
}
|
||||
for _, ip := range ip6s {
|
||||
assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"::/0\"} must have %q", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalhostOnly(t *testing.T) {
|
||||
set := NewNetSet()
|
||||
set.AddIPNet(net.IPNet{IP: net.ParseIP("127.0.0.0"), Mask: net.CIDRMask(8, 32)})
|
||||
set.AddIPNet(net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)})
|
||||
|
||||
included := []string{
|
||||
"127.0.0.1",
|
||||
"127.196.199.162",
|
||||
"127.78.85.40",
|
||||
"127.205.171.240",
|
||||
"127.121.187.93",
|
||||
"127.5.217.10",
|
||||
"127.22.172.251",
|
||||
"127.12.60.198",
|
||||
"127.110.240.252",
|
||||
"::1",
|
||||
}
|
||||
for _, ip := range included {
|
||||
assert.Truef(t, set.Has(net.ParseIP(ip)), "NetSet{\"127.0.0.0/8\", \"::1\"} must have %q", ip)
|
||||
}
|
||||
excluded := []string{
|
||||
"15.65.169.230",
|
||||
"196.72.207.65",
|
||||
"43.46.246.121",
|
||||
"69.147.193.224",
|
||||
"43.130.74.115",
|
||||
"8375:b8f9:70bf:a247:f55e:b0c0:c24f:eebe",
|
||||
"d325:aabf:ef49:2d46:ba47:1aa5:9e57:b0bf",
|
||||
"d2f6:831:170:e04e:bd6d:7ae7:e14a:71c3",
|
||||
"ca80:483f:bb7f:df2a:bceb:de5e:6129:9625",
|
||||
"f3fe:f771:9138:5d63:8e83:7033:2e6f:4762",
|
||||
}
|
||||
for _, ip := range excluded {
|
||||
assert.Falsef(t, set.Has(net.ParseIP(ip)), "NetSet{\"127.0.0.0/8\", \"::1\"} must not have %q", ip)
|
||||
}
|
||||
}
|
39
pkg/ip/parse_ip_net.go
Normal file
39
pkg/ip/parse_ip_net.go
Normal file
@ -0,0 +1,39 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseIPNet(s string) *net.IPNet {
|
||||
if !strings.ContainsRune(s, '/') {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mask net.IPMask
|
||||
switch {
|
||||
case ip.To4() != nil:
|
||||
mask = net.CIDRMask(32, 32)
|
||||
case ip.To16() != nil:
|
||||
mask = net.CIDRMask(128, 128)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: mask,
|
||||
}
|
||||
}
|
||||
|
||||
switch ip, ipNet, err := net.ParseCIDR(s); {
|
||||
case err != nil:
|
||||
return nil
|
||||
case !ipNet.IP.Equal(ip):
|
||||
return nil
|
||||
default:
|
||||
return ipNet
|
||||
}
|
||||
}
|
@ -58,6 +58,14 @@ func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, err
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// GetClientIP obtains the perceived end-user IP address from headers if p != nil else from req.RemoteAddr.
|
||||
func GetClientIP(p ipapi.RealClientIPParser, req *http.Request) (net.IP, error) {
|
||||
if p != nil {
|
||||
return p.GetRealClientIP(req.Header)
|
||||
}
|
||||
return getRemoteIP(req)
|
||||
}
|
||||
|
||||
// getRemoteIP obtains the IP of the low-level connected network host
|
||||
func getRemoteIP(req *http.Request) (net.IP, error) {
|
||||
if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
|
||||
|
@ -225,6 +225,16 @@ func Validate(o *options.Options) error {
|
||||
})
|
||||
}
|
||||
|
||||
if len(o.TrustedIPs) > 0 && o.ReverseProxy {
|
||||
fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
|
||||
}
|
||||
|
||||
for i, ipStr := range o.TrustedIPs {
|
||||
if nil == ip.ParseIPNet(ipStr) {
|
||||
msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr))
|
||||
}
|
||||
}
|
||||
|
||||
if len(msgs) != 0 {
|
||||
return fmt.Errorf("invalid configuration:\n %s",
|
||||
strings.Join(msgs, "\n "))
|
||||
|
@ -2,6 +2,7 @@ package validation
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -352,6 +353,45 @@ func TestRealClientIPHeader(t *testing.T) {
|
||||
assert.Nil(t, o.GetRealClientIPParser())
|
||||
}
|
||||
|
||||
func TestIPCIDRSetOption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
trustedIPs []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"TestSomeIPs",
|
||||
[]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"},
|
||||
nil,
|
||||
}, {
|
||||
"TestOverlappingIPs",
|
||||
[]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"},
|
||||
nil,
|
||||
}, {
|
||||
"TestInvalidIPs",
|
||||
[]string{"[::1]", "alkwlkbn/32"},
|
||||
errors.New(
|
||||
"invalid configuration:\n" +
|
||||
" trusted_ips[0] ([::1]) could not be recognized\n" +
|
||||
" trusted_ips[1] (alkwlkbn/32) could not be recognized",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := testOptions()
|
||||
o.TrustedIPs = tt.trustedIPs
|
||||
err := Validate(o)
|
||||
if tt.err == nil {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.Equal(t, tt.err.Error(), err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCAFilesError(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "absent.*.crt")
|
||||
assert.NoError(t, err)
|
||||
|
Loading…
Reference in New Issue
Block a user