-
Notifications
You must be signed in to change notification settings - Fork 617
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add basic ip centric access control on routes
- Loading branch information
Showing
10 changed files
with
301 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
--- | ||
title: "Access Control" | ||
since: "1.5.8" | ||
--- | ||
|
||
fabio supports basic ip centric access control per route. You may | ||
specify one of `allow` or `deny` options per route to control access. | ||
Currently only source ip control is available. | ||
|
||
<!--more--> | ||
|
||
To allow access to a route from clients within the `192.168.1.0/24` | ||
and `10.0.0.0/8` subnet you would add the following option: | ||
|
||
``` | ||
allow=ip:192.168.1.0/24,ip:10.0.0.0/8 | ||
``` | ||
|
||
With this specified only clients sourced from those two subnets will | ||
be allowed. All other requests to that route will be denied. | ||
|
||
|
||
Inversely, to only deny a specific set of clients you can use the | ||
following option syntax: | ||
|
||
``` | ||
deny=ip:1.2.3.4/32,100.123.0.0/16 | ||
``` | ||
|
||
With this configuration access will be denied to any clients with | ||
the `1.2.3.4` address or coming from the `100.123.0.0/16` network. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package route | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
const ( | ||
ipAllowTag = "allow:ip" | ||
ipDenyTag = "deny:ip" | ||
) | ||
|
||
// AccessDeniedHTTP checks rules on the target for HTTP proxy routes. | ||
func (t *Target) AccessDeniedHTTP(r *http.Request) bool { | ||
host, _, err := net.SplitHostPort(r.RemoteAddr) | ||
|
||
if err != nil { | ||
log.Printf("[ERROR] Failed to get host from RemoteAddr %s: %s", | ||
r.RemoteAddr, err.Error()) | ||
return false | ||
} | ||
|
||
// prefer xff header if set | ||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" && xff != host { | ||
host = xff | ||
} | ||
|
||
// currently only one function - more may be added in the future | ||
return t.denyByIP(net.ParseIP(host)) | ||
} | ||
|
||
// AccessDeniedTCP checks rules on the target for TCP proxy routes. | ||
func (t *Target) AccessDeniedTCP(c net.Conn) bool { | ||
// currently only one function - more may be added in the future | ||
return t.denyByIP(net.ParseIP(c.RemoteAddr().String())) | ||
} | ||
|
||
func (t *Target) denyByIP(ip net.IP) bool { | ||
if ip == nil || t.accessRules == nil { | ||
return false | ||
} | ||
|
||
// check allow (whitelist) first if it exists | ||
if _, ok := t.accessRules[ipAllowTag]; ok { | ||
var block *net.IPNet | ||
for _, x := range t.accessRules[ipAllowTag] { | ||
if block, ok = x.(*net.IPNet); !ok { | ||
log.Print("[ERROR] failed to assert ip block while checking allow rule for ", t.Service) | ||
continue | ||
} | ||
if block.Contains(ip) { | ||
// specific allow matched - allow this request | ||
return false | ||
} | ||
} | ||
// we checked all the blocks - deny this request | ||
return true | ||
} | ||
|
||
// still going - check deny (blacklist) if it exists | ||
if _, ok := t.accessRules[ipDenyTag]; ok { | ||
var block *net.IPNet | ||
for _, x := range t.accessRules[ipDenyTag] { | ||
if block, ok = x.(*net.IPNet); !ok { | ||
log.Print("[INFO] failed to assert ip block while checking deny rule for ", t.Service) | ||
continue | ||
} | ||
if block.Contains(ip) { | ||
// specific deny matched - deny this request | ||
return true | ||
} | ||
} | ||
} | ||
|
||
// default - do not deny | ||
return false | ||
} | ||
|
||
func (t *Target) parseAccessRule(allowDeny string) error { | ||
var accessTag string | ||
var temps []string | ||
|
||
// init rules if needed | ||
if t.accessRules == nil { | ||
t.accessRules = make(map[string][]interface{}) | ||
} | ||
|
||
// loop over rule elements | ||
for _, c := range strings.Split(t.Opts[allowDeny], ",") { | ||
if temps = strings.SplitN(c, ":", 2); len(temps) != 2 { | ||
return fmt.Errorf("invalid access item, expected <type>:<data>, got %s", temps) | ||
} | ||
accessTag = allowDeny + ":" + strings.ToLower(temps[0]) | ||
switch accessTag { | ||
case ipAllowTag, ipDenyTag: | ||
_, net, err := net.ParseCIDR(strings.TrimSpace(temps[1])) | ||
if err != nil { | ||
return fmt.Errorf("failed to parse CIDR %s with error: %s", | ||
c, err.Error()) | ||
} | ||
// add element to rule map | ||
t.accessRules[accessTag] = append(t.accessRules[accessTag], net) | ||
default: | ||
return fmt.Errorf("unknown access item type: %s", temps[0]) | ||
} | ||
} | ||
return nil | ||
|
||
} | ||
|
||
func (t *Target) processAccessRules() error { | ||
if t.Opts["allow"] != "" && t.Opts["deny"] != "" { | ||
return errors.New("specifying allow and deny on the same route is not supported") | ||
} | ||
|
||
for _, allowDeny := range []string{"allow", "deny"} { | ||
if t.Opts[allowDeny] != "" { | ||
if err := t.parseAccessRule(allowDeny); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package route | ||
|
||
import ( | ||
"net" | ||
"testing" | ||
) | ||
|
||
func TestAccessRules_parseAccessRule(t *testing.T) { | ||
tests := []struct { | ||
desc string | ||
allowDeny string | ||
fail bool | ||
}{ | ||
{ | ||
desc: "parseAccessRuleGood", | ||
allowDeny: "ip:10.0.0.0/8,ip:192.168.0.0/24,ip:1.2.3.4/32", | ||
}, | ||
{ | ||
desc: "parseAccessRuleBadType", | ||
allowDeny: "x:10.0.0.0/8", | ||
fail: true, | ||
}, | ||
{ | ||
desc: "parseAccessRuleIncompleteIP", | ||
allowDeny: "ip:10/8", | ||
fail: true, | ||
}, | ||
{ | ||
desc: "parseAccessRuleBadCIDR", | ||
allowDeny: "ip:10.0.0.0/255", | ||
fail: true, | ||
}, | ||
} | ||
|
||
for i, tt := range tests { | ||
tt := tt // capture loop var | ||
|
||
t.Run(tt.desc, func(t *testing.T) { | ||
for _, ad := range []string{"allow", "deny"} { | ||
tgt := &Target{Opts: map[string]string{ad: tt.allowDeny}} | ||
err := tgt.parseAccessRule(ad) | ||
if err != nil && !tt.fail { | ||
t.Errorf("%d: %s\nfailed: %s", i, tt.desc, err.Error()) | ||
return | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestAccessRules_denyByIP(t *testing.T) { | ||
tests := []struct { | ||
desc string | ||
target *Target | ||
remote net.IP | ||
denied bool | ||
}{ | ||
{ | ||
desc: "denyByIPAllowAllowed", | ||
target: &Target{ | ||
Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, | ||
}, | ||
remote: net.ParseIP("10.10.0.1"), | ||
denied: false, | ||
}, | ||
{ | ||
desc: "denyByIPAllowDenied", | ||
target: &Target{ | ||
Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, | ||
}, | ||
remote: net.ParseIP("1.2.3.4"), | ||
denied: true, | ||
}, | ||
{ | ||
desc: "denyByIPDenyDenied", | ||
target: &Target{ | ||
Opts: map[string]string{"deny": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, | ||
}, | ||
remote: net.ParseIP("10.10.0.1"), | ||
denied: true, | ||
}, | ||
{ | ||
desc: "denyByIPDenyAllow", | ||
target: &Target{ | ||
Opts: map[string]string{"deny": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, | ||
}, | ||
remote: net.ParseIP("1.2.3.4"), | ||
denied: false, | ||
}, | ||
} | ||
|
||
for i, tt := range tests { | ||
tt := tt // capture loop var | ||
|
||
t.Run(tt.desc, func(t *testing.T) { | ||
if err := tt.target.processAccessRules(); err != nil { | ||
t.Errorf("%d: %s - failed to process access rules: %s", | ||
i, tt.desc, err.Error()) | ||
} | ||
if deny := tt.target.denyByIP(tt.remote); deny != tt.denied { | ||
t.Errorf("%d: %s\ngot denied: %t\nwant denied: %t\n", | ||
i, tt.desc, deny, tt.denied) | ||
return | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters