Add trusted whitelist proxy protocol
This commit is contained in:
parent
93a1db77c5
commit
6287a3dd53
9 changed files with 249 additions and 143 deletions
|
@ -84,7 +84,9 @@ func TestDo_globalConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"},
|
WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"},
|
||||||
Compress: true,
|
Compress: true,
|
||||||
ProxyProtocol: true,
|
ProxyProtocol: &configuration.ProxyProtocol{
|
||||||
|
TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"fii": {
|
"fii": {
|
||||||
Network: "fii Network",
|
Network: "fii Network",
|
||||||
|
@ -125,7 +127,9 @@ func TestDo_globalConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"},
|
WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"},
|
||||||
Compress: true,
|
Compress: true,
|
||||||
ProxyProtocol: true,
|
ProxyProtocol: &configuration.ProxyProtocol{
|
||||||
|
TrustedIPs: []string{"127.0.0.1/32", "192.168.0.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Cluster = &types.Cluster{
|
config.Cluster = &types.Cluster{
|
||||||
|
|
|
@ -231,7 +231,14 @@ func (ep *EntryPoints) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
compress := toBool(result, "Compress")
|
compress := toBool(result, "Compress")
|
||||||
proxyProtocol := toBool(result, "ProxyProtocol")
|
|
||||||
|
var proxyProtocol *ProxyProtocol
|
||||||
|
if len(result["ProxyProtocol"]) > 0 {
|
||||||
|
trustedIPs := strings.Split(result["ProxyProtocol"], ",")
|
||||||
|
proxyProtocol = &ProxyProtocol{
|
||||||
|
TrustedIPs: trustedIPs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(*ep)[result["Name"]] = &EntryPoint{
|
(*ep)[result["Name"]] = &EntryPoint{
|
||||||
Address: result["Address"],
|
Address: result["Address"],
|
||||||
|
@ -246,7 +253,7 @@ func (ep *EntryPoints) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEntryPointsConfiguration(value string) (map[string]string, error) {
|
func parseEntryPointsConfiguration(value string) (map[string]string, error) {
|
||||||
regex := regexp.MustCompile(`(?:Name:(?P<Name>\S*))\s*(?:Address:(?P<Address>\S*))?\s*(?:TLS:(?P<TLS>\S*))?\s*(?P<TLSACME>TLS)?\s*(?:CA:(?P<CA>\S*))?\s*(?:Redirect\.EntryPoint:(?P<RedirectEntryPoint>\S*))?\s*(?:Redirect\.Regex:(?P<RedirectRegex>\S*))?\s*(?:Redirect\.Replacement:(?P<RedirectReplacement>\S*))?\s*(?:Compress:(?P<Compress>\S*))?\s*(?:WhiteListSourceRange:(?P<WhiteListSourceRange>\S*))?\s*(?:ProxyProtocol:(?P<ProxyProtocol>\S*))?`)
|
regex := regexp.MustCompile(`(?:Name:(?P<Name>\S*))\s*(?:Address:(?P<Address>\S*))?\s*(?:TLS:(?P<TLS>\S*))?\s*(?P<TLSACME>TLS)?\s*(?:CA:(?P<CA>\S*))?\s*(?:Redirect\.EntryPoint:(?P<RedirectEntryPoint>\S*))?\s*(?:Redirect\.Regex:(?P<RedirectRegex>\S*))?\s*(?:Redirect\.Replacement:(?P<RedirectReplacement>\S*))?\s*(?:Compress:(?P<Compress>\S*))?\s*(?:WhiteListSourceRange:(?P<WhiteListSourceRange>\S*))?\s*(?:ProxyProtocol\.TrustedIPs:(?P<ProxyProtocol>\S*))?`)
|
||||||
match := regex.FindAllStringSubmatch(value, -1)
|
match := regex.FindAllStringSubmatch(value, -1)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
return nil, fmt.Errorf("bad EntryPoints format: %s", value)
|
return nil, fmt.Errorf("bad EntryPoints format: %s", value)
|
||||||
|
@ -293,8 +300,8 @@ type EntryPoint struct {
|
||||||
Redirect *Redirect `export:"true"`
|
Redirect *Redirect `export:"true"`
|
||||||
Auth *types.Auth `export:"true"`
|
Auth *types.Auth `export:"true"`
|
||||||
WhitelistSourceRange []string
|
WhitelistSourceRange []string
|
||||||
Compress bool `export:"true"`
|
Compress bool `export:"true"`
|
||||||
ProxyProtocol bool `export:"true"`
|
ProxyProtocol *ProxyProtocol `export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect configures a redirection of an entry point to another, or to an URL
|
// Redirect configures a redirection of an entry point to another, or to an URL
|
||||||
|
@ -443,3 +450,8 @@ type ForwardingTimeouts struct {
|
||||||
DialTimeout flaeg.Duration `description:"The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists" export:"true"`
|
DialTimeout flaeg.Duration `description:"The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists" export:"true"`
|
||||||
ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"`
|
ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyProtocol contains Proxy-Protocol configuration
|
||||||
|
type ProxyProtocol struct {
|
||||||
|
TrustedIPs []string
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all parameters",
|
name: "all parameters",
|
||||||
value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol:true",
|
value: "Name:foo Address:bar TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1",
|
||||||
expectedResult: map[string]string{
|
expectedResult: map[string]string{
|
||||||
"Name": "foo",
|
"Name": "foo",
|
||||||
"Address": "bar",
|
"Address": "bar",
|
||||||
|
@ -27,18 +27,10 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
|
||||||
"RedirectRegex": "RedirectRegex",
|
"RedirectRegex": "RedirectRegex",
|
||||||
"RedirectReplacement": "RedirectReplacement",
|
"RedirectReplacement": "RedirectReplacement",
|
||||||
"WhiteListSourceRange": "WhiteListSourceRange",
|
"WhiteListSourceRange": "WhiteListSourceRange",
|
||||||
"ProxyProtocol": "true",
|
"ProxyProtocol": "192.168.0.1",
|
||||||
"Compress": "true",
|
"Compress": "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "proxy protocol on",
|
|
||||||
value: "Name:foo ProxyProtocol:on",
|
|
||||||
expectedResult: map[string]string{
|
|
||||||
"Name": "foo",
|
|
||||||
"ProxyProtocol": "on",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "compress on",
|
name: "compress on",
|
||||||
value: "Name:foo Compress:on",
|
value: "Name:foo Compress:on",
|
||||||
|
@ -142,7 +134,7 @@ func TestEntryPoints_Set(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all parameters",
|
name: "all parameters",
|
||||||
expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol:true",
|
expression: "Name:foo Address:bar TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1",
|
||||||
expectedEntryPointName: "foo",
|
expectedEntryPointName: "foo",
|
||||||
expectedEntryPoint: &EntryPoint{
|
expectedEntryPoint: &EntryPoint{
|
||||||
Address: "bar",
|
Address: "bar",
|
||||||
|
@ -151,8 +143,10 @@ func TestEntryPoints_Set(t *testing.T) {
|
||||||
Regex: "RedirectRegex",
|
Regex: "RedirectRegex",
|
||||||
Replacement: "RedirectReplacement",
|
Replacement: "RedirectReplacement",
|
||||||
},
|
},
|
||||||
Compress: true,
|
Compress: true,
|
||||||
ProxyProtocol: true,
|
ProxyProtocol: &ProxyProtocol{
|
||||||
|
TrustedIPs: []string{"192.168.0.1"},
|
||||||
|
},
|
||||||
WhitelistSourceRange: []string{"Range"},
|
WhitelistSourceRange: []string{"Range"},
|
||||||
TLS: &TLS{
|
TLS: &TLS{
|
||||||
ClientCAFiles: []string{"car"},
|
ClientCAFiles: []string{"car"},
|
||||||
|
|
|
@ -185,16 +185,20 @@ To enable IP whitelisting at the entrypoint level.
|
||||||
[entryPoints]
|
[entryPoints]
|
||||||
[entryPoints.http]
|
[entryPoints.http]
|
||||||
address = ":80"
|
address = ":80"
|
||||||
whiteListSourceRange = ["127.0.0.1/32"]
|
whiteListSourceRange = ["127.0.0.1/32", "192.168.1.7"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## ProxyProtocol Support
|
## ProxyProtocol Support
|
||||||
|
|
||||||
To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support.
|
To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support.
|
||||||
|
Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here.
|
||||||
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[entryPoints]
|
[entryPoints]
|
||||||
[entryPoints.http]
|
[entryPoints.http]
|
||||||
address = ":80"
|
address = ":80"
|
||||||
proxyprotocol = true
|
[entryPoints.http.proxyProtocol]
|
||||||
|
trustedIPs = ["127.0.0.1/32", "192.168.1.7"]
|
||||||
```
|
```
|
||||||
|
²
|
|
@ -6,80 +6,70 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
|
"github.com/containous/traefik/whitelist"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPWhitelister is a middleware that provides Checks of the Requesting IP against a set of Whitelists
|
// IPWhiteLister is a middleware that provides Checks of the Requesting IP against a set of Whitelists
|
||||||
type IPWhitelister struct {
|
type IPWhiteLister struct {
|
||||||
handler negroni.Handler
|
handler negroni.Handler
|
||||||
whitelists []*net.IPNet
|
whiteLister *whitelist.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIPWhitelister builds a new IPWhitelister given a list of CIDR-Strings to whitelist
|
// NewIPWhitelister builds a new IPWhiteLister given a list of CIDR-Strings to whitelist
|
||||||
func NewIPWhitelister(whitelistStrings []string) (*IPWhitelister, error) {
|
func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) {
|
||||||
|
|
||||||
if len(whitelistStrings) == 0 {
|
if len(whitelistStrings) == 0 {
|
||||||
return nil, errors.New("no whitelists provided")
|
return nil, errors.New("no whitelists provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
whitelister := IPWhitelister{}
|
whiteLister := IPWhiteLister{}
|
||||||
|
|
||||||
for _, whitelistString := range whitelistStrings {
|
ip, err := whitelist.NewIP(whitelistStrings)
|
||||||
_, whitelist, err := net.ParseCIDR(whitelistString)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err)
|
||||||
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err)
|
|
||||||
}
|
|
||||||
whitelister.whitelists = append(whitelister.whitelists, whitelist)
|
|
||||||
}
|
}
|
||||||
|
whiteLister.whiteLister = ip
|
||||||
|
|
||||||
whitelister.handler = negroni.HandlerFunc(whitelister.handle)
|
whiteLister.handler = negroni.HandlerFunc(whiteLister.handle)
|
||||||
log.Debugf("configured %u IP whitelists: %s", len(whitelister.whitelists), whitelister.whitelists)
|
log.Debugf("configured %u IP whitelists: %s", len(whitelistStrings), whitelistStrings)
|
||||||
|
|
||||||
return &whitelister, nil
|
return &whiteLister, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (whitelister *IPWhitelister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
remoteIP, err := ipFromRemoteAddr(r.RemoteAddr)
|
ipAddress, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr)
|
log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr)
|
||||||
reject(w)
|
reject(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, whitelist := range whitelister.whitelists {
|
allowed, ip, err := wl.whiteLister.Contains(ipAddress)
|
||||||
if whitelist.Contains(*remoteIP) {
|
if err != nil {
|
||||||
log.Debugf("source-IP %s matched whitelist %s - passing", remoteIP, whitelist)
|
log.Debugf("source-IP %s matched none of the whitelists - rejecting", ipAddress)
|
||||||
next.ServeHTTP(w, r)
|
reject(w)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("source-IP %s matched none of the whitelists - rejecting", remoteIP)
|
if allowed {
|
||||||
|
log.Debugf("source-IP %s matched whitelist %s - passing", ipAddress, wl.whiteLister)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("source-IP %s matched none of the whitelists - rejecting", ip)
|
||||||
reject(w)
|
reject(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wl *IPWhiteLister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
|
wl.handler.ServeHTTP(rw, r, next)
|
||||||
|
}
|
||||||
|
|
||||||
func reject(w http.ResponseWriter) {
|
func reject(w http.ResponseWriter) {
|
||||||
statusCode := http.StatusForbidden
|
statusCode := http.StatusForbidden
|
||||||
|
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
w.Write([]byte(http.StatusText(statusCode)))
|
w.Write([]byte(http.StatusText(statusCode)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipFromRemoteAddr(addr string) (*net.IP, error) {
|
|
||||||
ip, _, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't extract IP/Port from address %s: %s", addr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userIP := net.ParseIP(ip)
|
|
||||||
if userIP == nil {
|
|
||||||
return nil, fmt.Errorf("can't parse IP from address %s", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &userIP, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (whitelister *IPWhitelister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
|
||||||
whitelister.handler.ServeHTTP(rw, r, next)
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -32,6 +33,7 @@ import (
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/server/cookie"
|
"github.com/containous/traefik/server/cookie"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
|
"github.com/containous/traefik/whitelist"
|
||||||
"github.com/streamrail/concurrent-map"
|
"github.com/streamrail/concurrent-map"
|
||||||
thoas_stats "github.com/thoas/stats"
|
thoas_stats "github.com/thoas/stats"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
|
@ -652,8 +654,22 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if entryPoint.ProxyProtocol {
|
if entryPoint.ProxyProtocol != nil {
|
||||||
listener = &proxyproto.Listener{Listener: listener}
|
IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Error creating whitelist: %s", err)
|
||||||
|
}
|
||||||
|
log.Infof("Enabling ProxyProtocol for trusted IPs %v", entryPoint.ProxyProtocol.TrustedIPs)
|
||||||
|
listener = &proxyproto.Listener{
|
||||||
|
Listener: listener,
|
||||||
|
SourceCheck: func(addr net.Addr) (bool, error) {
|
||||||
|
ip, ok := addr.(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("Type error %v", addr)
|
||||||
|
}
|
||||||
|
return IPs.ContainsIP(ip.IP)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
|
|
|
@ -355,7 +355,7 @@ func TestNewServerWithWhitelistSourceRange(t *testing.T) {
|
||||||
"foo",
|
"foo",
|
||||||
},
|
},
|
||||||
middlewareConfigured: false,
|
middlewareConfigured: false,
|
||||||
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: foo",
|
errMessage: "parsing CIDR whitelist [foo]: parsing CIDR whitelist <nil>: invalid CIDR address: foo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +536,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) {
|
||||||
handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni)
|
handler := srvEntryPoint.httpServer.Handler.(*negroni.Negroni)
|
||||||
found := false
|
found := false
|
||||||
for _, handler := range handler.Handlers() {
|
for _, handler := range handler.Handlers() {
|
||||||
if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhitelister)(nil)) {
|
if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhiteLister)(nil)) {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
75
whitelist/ip.go
Normal file
75
whitelist/ip.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package whitelist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IP allows to check that addresses are in a white list
|
||||||
|
type IP struct {
|
||||||
|
whiteListsIPs []*net.IP
|
||||||
|
whiteListsNet []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIP builds a new IP given a list of CIDR-Strings to whitelist
|
||||||
|
func NewIP(whitelistStrings []string) (*IP, error) {
|
||||||
|
if len(whitelistStrings) == 0 {
|
||||||
|
return nil, errors.New("no whiteListsNet provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := IP{}
|
||||||
|
|
||||||
|
for _, whitelistString := range whitelistStrings {
|
||||||
|
ipAddr := net.ParseIP(whitelistString)
|
||||||
|
if ipAddr != nil {
|
||||||
|
ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr)
|
||||||
|
} else {
|
||||||
|
_, whitelist, err := net.ParseCIDR(whitelistString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err)
|
||||||
|
}
|
||||||
|
ip.whiteListsNet = append(ip.whiteListsNet, whitelist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if provided address is in the white list
|
||||||
|
func (ip *IP) Contains(addr string) (bool, net.IP, error) {
|
||||||
|
ipAddr, err := ipFromRemoteAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contains, err := ip.ContainsIP(ipAddr)
|
||||||
|
return contains, ipAddr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsIP checks if provided address is in the white list
|
||||||
|
func (ip *IP) ContainsIP(addr net.IP) (bool, error) {
|
||||||
|
for _, whiteListIP := range ip.whiteListsIPs {
|
||||||
|
if whiteListIP.Equal(addr) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, whiteListNet := range ip.whiteListsNet {
|
||||||
|
if whiteListNet.Contains(addr) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipFromRemoteAddr(addr string) (net.IP, error) {
|
||||||
|
userIP := net.ParseIP(addr)
|
||||||
|
if userIP == nil {
|
||||||
|
return nil, fmt.Errorf("can't parse IP from address %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIP, nil
|
||||||
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
package middlewares
|
package whitelist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containous/traefik/testhelpers"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/negroni"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewIPWhitelister(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
desc string
|
desc string
|
||||||
whitelistStrings []string
|
whitelistStrings []string
|
||||||
|
@ -24,12 +19,12 @@ func TestNewIPWhitelister(t *testing.T) {
|
||||||
desc: "nil whitelist",
|
desc: "nil whitelist",
|
||||||
whitelistStrings: nil,
|
whitelistStrings: nil,
|
||||||
expectedWhitelists: nil,
|
expectedWhitelists: nil,
|
||||||
errMessage: "no whitelists provided",
|
errMessage: "no whiteListsNet provided",
|
||||||
}, {
|
}, {
|
||||||
desc: "empty whitelist",
|
desc: "empty whitelist",
|
||||||
whitelistStrings: []string{},
|
whitelistStrings: []string{},
|
||||||
expectedWhitelists: nil,
|
expectedWhitelists: nil,
|
||||||
errMessage: "no whitelists provided",
|
errMessage: "no whiteListsNet provided",
|
||||||
}, {
|
}, {
|
||||||
desc: "whitelist containing empty string",
|
desc: "whitelist containing empty string",
|
||||||
whitelistStrings: []string{
|
whitelistStrings: []string{
|
||||||
|
@ -80,12 +75,12 @@ func TestNewIPWhitelister(t *testing.T) {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
whitelister, err := NewIPWhitelister(test.whitelistStrings)
|
whitelister, err := NewIP(test.whitelistStrings)
|
||||||
if test.errMessage != "" {
|
if test.errMessage != "" {
|
||||||
require.EqualError(t, err, test.errMessage)
|
require.EqualError(t, err, test.errMessage)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for index, actual := range whitelister.whitelists {
|
for index, actual := range whitelister.whiteListsNet {
|
||||||
expected := test.expectedWhitelists[index]
|
expected := test.expectedWhitelists[index]
|
||||||
assert.Equal(t, expected.IP, actual.IP)
|
assert.Equal(t, expected.IP, actual.IP)
|
||||||
assert.Equal(t, expected.Mask.String(), actual.Mask.String())
|
assert.Equal(t, expected.Mask.String(), actual.Mask.String())
|
||||||
|
@ -95,7 +90,7 @@ func TestNewIPWhitelister(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPWhitelisterHandle(t *testing.T) {
|
func TestIsAllowed(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
desc string
|
desc string
|
||||||
whitelistStrings []string
|
whitelistStrings []string
|
||||||
|
@ -122,6 +117,23 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "IPv4 single IP",
|
desc: "IPv4 single IP",
|
||||||
|
whitelistStrings: []string{
|
||||||
|
"8.8.8.8",
|
||||||
|
},
|
||||||
|
passIPs: []string{
|
||||||
|
"8.8.8.8",
|
||||||
|
},
|
||||||
|
rejectIPs: []string{
|
||||||
|
"8.8.8.7",
|
||||||
|
"8.8.8.9",
|
||||||
|
"8.8.8.0",
|
||||||
|
"8.8.8.255",
|
||||||
|
"4.4.4.4",
|
||||||
|
"127.0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "IPv4 Net single IP",
|
||||||
whitelistStrings: []string{
|
whitelistStrings: []string{
|
||||||
"8.8.8.8/32",
|
"8.8.8.8/32",
|
||||||
},
|
},
|
||||||
|
@ -167,16 +179,16 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"2a03:4000:6:d080::/64",
|
"2a03:4000:6:d080::/64",
|
||||||
},
|
},
|
||||||
passIPs: []string{
|
passIPs: []string{
|
||||||
"[2a03:4000:6:d080::]",
|
"2a03:4000:6:d080::",
|
||||||
"[2a03:4000:6:d080::1]",
|
"2a03:4000:6:d080::1",
|
||||||
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
|
"2a03:4000:6:d080:dead:beef:ffff:ffff",
|
||||||
"[2a03:4000:6:d080::42]",
|
"2a03:4000:6:d080::42",
|
||||||
},
|
},
|
||||||
rejectIPs: []string{
|
rejectIPs: []string{
|
||||||
"[2a03:4000:7:d080::]",
|
"2a03:4000:7:d080::",
|
||||||
"[2a03:4000:7:d080::1]",
|
"2a03:4000:7:d080::1",
|
||||||
"[fe80::]",
|
"fe80::",
|
||||||
"[4242::1]",
|
"4242::1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -185,12 +197,12 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"2a03:4000:6:d080::42/128",
|
"2a03:4000:6:d080::42/128",
|
||||||
},
|
},
|
||||||
passIPs: []string{
|
passIPs: []string{
|
||||||
"[2a03:4000:6:d080::42]",
|
"2a03:4000:6:d080::42",
|
||||||
},
|
},
|
||||||
rejectIPs: []string{
|
rejectIPs: []string{
|
||||||
"[2a03:4000:6:d080::1]",
|
"2a03:4000:6:d080::1",
|
||||||
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
|
"2a03:4000:6:d080:dead:beef:ffff:ffff",
|
||||||
"[2a03:4000:6:d080::43]",
|
"2a03:4000:6:d080::43",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -200,18 +212,18 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"fe80::/16",
|
"fe80::/16",
|
||||||
},
|
},
|
||||||
passIPs: []string{
|
passIPs: []string{
|
||||||
"[2a03:4000:6:d080::]",
|
"2a03:4000:6:d080::",
|
||||||
"[2a03:4000:6:d080::1]",
|
"2a03:4000:6:d080::1",
|
||||||
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
|
"2a03:4000:6:d080:dead:beef:ffff:ffff",
|
||||||
"[2a03:4000:6:d080::42]",
|
"2a03:4000:6:d080::42",
|
||||||
"[fe80::1]",
|
"fe80::1",
|
||||||
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]",
|
"fe80:aa00:00bb:4232:ff00:eeee:00ff:1111",
|
||||||
"[fe80::fe80]",
|
"fe80::fe80",
|
||||||
},
|
},
|
||||||
rejectIPs: []string{
|
rejectIPs: []string{
|
||||||
"[2a03:4000:7:d080::]",
|
"2a03:4000:7:d080::",
|
||||||
"[2a03:4000:7:d080::1]",
|
"2a03:4000:7:d080::1",
|
||||||
"[4242::1]",
|
"4242::1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -223,13 +235,13 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"8.8.8.8/8",
|
"8.8.8.8/8",
|
||||||
},
|
},
|
||||||
passIPs: []string{
|
passIPs: []string{
|
||||||
"[2a03:4000:6:d080::]",
|
"2a03:4000:6:d080::",
|
||||||
"[2a03:4000:6:d080::1]",
|
"2a03:4000:6:d080::1",
|
||||||
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
|
"2a03:4000:6:d080:dead:beef:ffff:ffff",
|
||||||
"[2a03:4000:6:d080::42]",
|
"2a03:4000:6:d080::42",
|
||||||
"[fe80::1]",
|
"fe80::1",
|
||||||
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]",
|
"fe80:aa00:00bb:4232:ff00:eeee:00ff:1111",
|
||||||
"[fe80::fe80]",
|
"fe80::fe80",
|
||||||
"1.2.3.1",
|
"1.2.3.1",
|
||||||
"1.2.3.32",
|
"1.2.3.32",
|
||||||
"1.2.3.156",
|
"1.2.3.156",
|
||||||
|
@ -240,9 +252,9 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"8.255.255.255",
|
"8.255.255.255",
|
||||||
},
|
},
|
||||||
rejectIPs: []string{
|
rejectIPs: []string{
|
||||||
"[2a03:4000:7:d080::]",
|
"2a03:4000:7:d080::",
|
||||||
"[2a03:4000:7:d080::1]",
|
"2a03:4000:7:d080::1",
|
||||||
"[4242::1]",
|
"4242::1",
|
||||||
"1.2.16.1",
|
"1.2.16.1",
|
||||||
"1.2.32.1",
|
"1.2.32.1",
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
@ -256,13 +268,6 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
"127.0.0.1/32",
|
"127.0.0.1/32",
|
||||||
},
|
},
|
||||||
passIPs: nil,
|
passIPs: nil,
|
||||||
rejectIPs: []string{
|
|
||||||
"foo",
|
|
||||||
"10.0.0.350",
|
|
||||||
"fe:::80",
|
|
||||||
"",
|
|
||||||
"\\&$§&/(",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,38 +275,44 @@ func TestIPWhitelisterHandle(t *testing.T) {
|
||||||
test := test
|
test := test
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
whitelister, err := NewIPWhitelister(test.whitelistStrings)
|
whiteLister, err := NewIP(test.whitelistStrings)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, whitelister)
|
require.NotNil(t, whiteLister)
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintln(w, "traefik")
|
|
||||||
})
|
|
||||||
n := negroni.New(whitelister)
|
|
||||||
n.UseHandler(handler)
|
|
||||||
|
|
||||||
for _, testIP := range test.passIPs {
|
for _, testIP := range test.passIPs {
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "/", nil)
|
allowed, ip, err := whiteLister.Contains(testIP)
|
||||||
|
require.NoError(t, err)
|
||||||
req.RemoteAddr = testIP + ":2342"
|
require.NotNil(t, ip, err)
|
||||||
recorder := httptest.NewRecorder()
|
assert.True(t, allowed, testIP+" should have passed "+test.desc)
|
||||||
n.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, recorder.Code, testIP+" should have passed "+test.desc)
|
|
||||||
assert.Contains(t, recorder.Body.String(), "traefik")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testIP := range test.rejectIPs {
|
for _, testIP := range test.rejectIPs {
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "/", nil)
|
allowed, ip, err := whiteLister.Contains(testIP)
|
||||||
|
require.NoError(t, err)
|
||||||
req.RemoteAddr = testIP + ":2342"
|
require.NotNil(t, ip, err)
|
||||||
recorder := httptest.NewRecorder()
|
assert.False(t, allowed, testIP+" should not have passed "+test.desc)
|
||||||
n.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusForbidden, recorder.Code, testIP+" should not have passed "+test.desc)
|
|
||||||
assert.NotContains(t, recorder.Body.String(), "traefik")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBrokenIPs(t *testing.T) {
|
||||||
|
brokenIPs := []string{
|
||||||
|
"foo",
|
||||||
|
"10.0.0.350",
|
||||||
|
"fe:::80",
|
||||||
|
"",
|
||||||
|
"\\&$§&/(",
|
||||||
|
}
|
||||||
|
|
||||||
|
whiteLister, err := NewIP([]string{"1.2.3.4/24"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, testIP := range brokenIPs {
|
||||||
|
_, ip, err := whiteLister.Contains(testIP)
|
||||||
|
assert.Error(t, err)
|
||||||
|
require.Nil(t, ip, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue