-
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.
Merge pull request #442 from myENA-feature/route-acl
Fixes #439
- Loading branch information
Showing
11 changed files
with
515 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,54 @@ | ||
--- | ||
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 `fe80::/10` subnet you would add the following option: | ||
|
||
``` | ||
allow=ip:192.168.1.0/24,ip:fe80::/10 | ||
``` | ||
|
||
With this specified only clients sourced from those two subnets will | ||
be allowed. All other requests to that route will be denied. | ||
|
||
|
||
Inversely, to deny a specific set of clients you can use the | ||
following option syntax: | ||
|
||
``` | ||
deny=ip:fe80::1234,100.123.0.0/16 | ||
``` | ||
|
||
With this configuration access will be denied to any clients with | ||
the `fe80::1234` address or coming from the `100.123.0.0/16` network. | ||
|
||
Single host addresses (addresses without a prefix) will have a | ||
`/32` prefix, for IPv4, or a `/128` prefix, for IPv6, added automatically. | ||
That means `1.2.3.4` is equivalent to `1.2.3.4/32` and `fe80::1234` | ||
is equivalent to `fe80::1234/128` when specifying | ||
address blocks for `allow` or `deny` rules. | ||
|
||
The source ip used for validation against the defined ruleset is | ||
taken from information available in the request. | ||
|
||
For `HTTP` requests the client `RemoteAddr` is always validated | ||
followed by the first element of the `X-Forwarded-For` header, if | ||
present. When either of these elements match an `allow` the request | ||
will be allowed; similarly when either element matches a `deny` the | ||
request will be denied. | ||
|
||
For `TCP` requests the source address of the network socket | ||
is used as the sole paramater for validation. | ||
|
||
If the inbound connection uses the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) | ||
to transmit the true source address of the client then it will | ||
be used for both `HTTP` and `TCP` connections for validating access. | ||
|
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
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,174 @@ | ||
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 { | ||
var ip net.IP | ||
host, _, err := net.SplitHostPort(r.RemoteAddr) | ||
|
||
if err != nil { | ||
log.Printf("[ERROR] failed to get host from remote header %s: %s", | ||
r.RemoteAddr, err.Error()) | ||
return false | ||
} | ||
|
||
if ip = net.ParseIP(host); ip == nil { | ||
log.Printf("[WARN] failed to parse remote address %s", host) | ||
} | ||
|
||
// check remote source and return if denied | ||
if t.denyByIP(ip) { | ||
return true | ||
} | ||
|
||
// check xff source if present | ||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" { | ||
// only use left-most element (client) | ||
xff = strings.TrimSpace(strings.SplitN(xff, ",", 2)[0]) | ||
// only continue if xff differs from host | ||
if xff != host { | ||
if ip = net.ParseIP(xff); ip == nil { | ||
log.Printf("[WARN] failed to parse xff address %s", xff) | ||
} | ||
// check xff source and return if denied | ||
if t.denyByIP(ip) { | ||
return true | ||
} | ||
} | ||
} | ||
|
||
// default allow | ||
return false | ||
} | ||
|
||
// AccessDeniedTCP checks rules on the target for TCP proxy routes. | ||
func (t *Target) AccessDeniedTCP(c net.Conn) bool { | ||
ip := net.ParseIP(c.RemoteAddr().String()) | ||
if t.denyByIP(ip) { | ||
return true | ||
} | ||
// default allow | ||
return false | ||
} | ||
|
||
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 | ||
log.Printf("[INFO] route rules denied access from %s to %s", | ||
ip.String(), t.URL.String()) | ||
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 | ||
log.Printf("[INFO] route rules denied access from %s to %s", | ||
ip.String(), t.URL.String()) | ||
return true | ||
} | ||
} | ||
} | ||
|
||
// default - do not deny | ||
return false | ||
} | ||
|
||
// ProcessAccessRules processes access rules from options specified on the target route | ||
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 | ||
} | ||
|
||
func (t *Target) parseAccessRule(allowDeny string) error { | ||
var accessTag string | ||
var temps []string | ||
var value string | ||
var ip net.IP | ||
|
||
// 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) | ||
} | ||
|
||
// form access type tag | ||
accessTag = allowDeny + ":" + strings.ToLower(strings.TrimSpace(temps[0])) | ||
|
||
// switch on formed access tag - currently only ip types are implemented | ||
switch accessTag { | ||
case ipAllowTag, ipDenyTag: | ||
if value = strings.TrimSpace(temps[1]); !strings.Contains(value, "/") { | ||
if ip = net.ParseIP(value); ip == nil { | ||
return fmt.Errorf("failed to parse IP %s", value) | ||
} | ||
if ip.To4() != nil { | ||
value = ip.String() + "/32" | ||
} else { | ||
value = ip.String() + "/128" | ||
} | ||
} | ||
_, net, err := net.ParseCIDR(value) | ||
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 | ||
} |
Oops, something went wrong.