Skip to content
This repository has been archived by the owner on May 14, 2020. It is now read-only.

Commit

Permalink
Implement support for whitelists, default-deny/allow (#8)
Browse files Browse the repository at this point in the history
Implement support for whitelists, default-deny/allow
  • Loading branch information
raulk authored May 10, 2019
2 parents 0d50705 + 54cb5e8 commit 0be077b
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 32 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ f := NewFilters()

// filter out addresses on the 192.168 subnet
_, ipnet, _ := net.ParseCIDR("192.168.0.0/16")
f.AddDialFilter(ipnet)

f.AddFilter(ipnet, ActionDeny)

// check if an address is blocked
lanaddr, _ := ma.NewMultiaddr("/ip4/192.168.0.17/tcp/4050")
fmt.Println(f.AddrBlocked(lanaddr))

// the default for a filter is accept, but we can change that
f.RemoveLiteral(ipnet)
f.DefaultAction = ActionDeny
fmt.Println(f.AddrBlocked(lanaddr))

// we can now allow the local LAN, denying everything else
f.AddFilter(ipnet, ActionAccept)
fmt.Println(f.AddrBlocked(lanaddr))
```

## Contribute
Expand Down
147 changes: 122 additions & 25 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,157 @@ import (
manet "github.com/multiformats/go-multiaddr-net"
)

// Action is an enum modelling all possible filter actions.
type Action int32

const (
ActionNone Action = iota // zero value.
ActionAccept
ActionDeny
)

type filterEntry struct {
f net.IPNet
action Action
}

// Filters is a structure representing a collection of accept/deny
// net.IPNet filters, together with the DefaultAction flag, which
// represents the default filter policy.
//
// Note that the last policy added to the Filters is authoritative.
type Filters struct {
DefaultAction Action

mu sync.RWMutex
filters map[string]*net.IPNet
filters []*filterEntry
}

// NewFilters constructs and returns a new set of net.IPNet filters.
// By default, the new filter accepts all addresses.
func NewFilters() *Filters {
return &Filters{
filters: make(map[string]*net.IPNet),
DefaultAction: ActionAccept,
filters: make([]*filterEntry, 0),
}
}

func (fs *Filters) find(ipnet net.IPNet) (int, *filterEntry) {
s := ipnet.String()
for idx, ft := range fs.filters {
if ft.f.String() == s {
return idx, ft
}
}
return -1, nil
}

// AddDialFilter adds a deny rule to this Filters set. Hosts
// matching the given net.IPNet filter will be denied, unless
// another rule is added which states that they should be accepted.
//
// No effort is made to prevent duplication of filters, or to simplify
// the filters list.
//
// DEPRECATED. Use AddFilter.
func (fs *Filters) AddDialFilter(f *net.IPNet) {
fs.AddFilter(*f, ActionDeny)
}

// AddFilter adds a rule to the Filters set, enforcing the desired action for
// the provided IPNet mask.
func (fs *Filters) AddFilter(ipnet net.IPNet, action Action) {
fs.mu.Lock()
defer fs.mu.Unlock()

if _, f := fs.find(ipnet); f != nil {
f.action = action
} else {
fs.filters = append(fs.filters, &filterEntry{ipnet, action})
}
}

// RemoveLiteral removes the first filter associated with the supplied IPNet,
// returning whether something was removed or not. It makes no distinction
// between whether the rule is an accept or a deny.
func (fs *Filters) RemoveLiteral(ipnet net.IPNet) (removed bool) {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.filters[f.String()] = f

if idx, _ := fs.find(ipnet); idx != -1 {
fs.filters = append(fs.filters[:idx], fs.filters[idx+1:]...)
return true
}
return false
}

func (f *Filters) AddrBlocked(a ma.Multiaddr) bool {
// AddrBlocked parses a ma.Multiaddr and, if a valid netip is found, it applies the
// Filter set rules, returning true if the given address should be denied, and false if
// the given address is accepted.
//
// If a parsing error occurs, or no filter matches, the Filters'
// default is returned.
//
// TODO: currently, the last filter to match wins always, but it shouldn't be that way.
// Instead, the highest-specific last filter should win; that way more specific filters
// override more general ones.
func (fs *Filters) AddrBlocked(a ma.Multiaddr) (deny bool) {
maddr := ma.Split(a)
if len(maddr) == 0 {
return false
return fs.DefaultAction == ActionDeny
}
netaddr, err := manet.ToNetAddr(maddr[0])
if err != nil {
// if we can't parse it, it's probably not blocked
return false
// if we can't parse it, it's probably not blocked.
return fs.DefaultAction == ActionDeny
}
netip := net.ParseIP(netaddr.String())
if netip == nil {
return false
return fs.DefaultAction == ActionDeny
}

f.mu.RLock()
defer f.mu.RUnlock()
for _, ft := range f.filters {
if ft.Contains(netip) {
return true
fs.mu.RLock()
defer fs.mu.RUnlock()

action := fs.DefaultAction
for _, ft := range fs.filters {
if ft.f.Contains(netip) {
action = ft.action
}
}
return false

return action == ActionDeny
}

// Filters returns the list of DENY net.IPNet masks. For backwards compatibility.
//
// A copy of the filters is made prior to returning, so the inner state is not exposed.
//
// DEPRECATED. Use FiltersForAction().
func (fs *Filters) Filters() (result []*net.IPNet) {
ffa := fs.FiltersForAction(ActionDeny)
for _, res := range ffa {
result = append(result, &res)
}
return result
}

func (f *Filters) Filters() []*net.IPNet {
var out []*net.IPNet
f.mu.RLock()
defer f.mu.RUnlock()
for _, ff := range f.filters {
out = append(out, ff)
func (fs *Filters) ActionForFilter(ipnet net.IPNet) (action Action, ok bool) {
if _, f := fs.find(ipnet); f != nil {
return f.action, true
}
return out
return ActionNone, false
}

func (f *Filters) Remove(ff *net.IPNet) {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.filters, ff.String())
// FiltersForAction returns the filters associated with the indicated action.
func (fs *Filters) FiltersForAction(action Action) (result []net.IPNet) {
fs.mu.RLock()
defer fs.mu.RUnlock()

for _, ff := range fs.filters {
if ff.action == action {
result = append(result, ff.f)
}
}
return result
}
128 changes: 123 additions & 5 deletions filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
ma "github.com/multiformats/go-multiaddr"
)

func TestFilter(t *testing.T) {
func TestFilterBlocking(t *testing.T) {
f := NewFilters()

_, ipnet, _ := net.ParseCIDR("0.1.2.3/24")
Expand All @@ -16,7 +16,14 @@ func TestFilter(t *testing.T) {
if len(filters) != 1 {
t.Fatal("Expected only 1 filter")
}
f.Remove(filters[0])

if a, ok := f.ActionForFilter(*ipnet); !ok || a != ActionDeny {
t.Fatal("Expected filter with DENY action")
}

if !f.RemoveLiteral(*filters[0]) {
t.Error("expected true value from RemoveLiteral")
}

for _, cidr := range []string{
"1.2.3.0/24",
Expand All @@ -28,6 +35,7 @@ func TestFilter(t *testing.T) {
f.AddDialFilter(ipnet)
}

// These addresses should all be blocked
for _, blocked := range []string{
"/ip4/1.2.3.4/tcp/123",
"/ip4/4.3.2.1/udp/123",
Expand All @@ -43,19 +51,129 @@ func TestFilter(t *testing.T) {
}
}

for _, notBlocked := range []string{
// test that other net intervals are not blocked
for _, addr := range []string{
"/ip4/1.2.4.1/tcp/123",
"/ip4/4.3.2.2/udp/123",
"/ip6/fe00::1/tcp/321",
"/ip6/fc00::2/udp/321",
"",
} {
maddr, err := ma.NewMultiaddr(notBlocked)
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
t.Error(err)
}
if f.AddrBlocked(maddr) {
t.Fatalf("expected %s to not be blocked", addr)
}
}
}

func TestFilterWhitelisting(t *testing.T) {
f := NewFilters()

// Add default reject filter
f.DefaultAction = ActionDeny

// Add a whitelist
_, ipnet, _ := net.ParseCIDR("1.2.3.0/24")
f.AddFilter(*ipnet, ActionAccept)

if a, ok := f.ActionForFilter(*ipnet); !ok || a != ActionAccept {
t.Fatal("Expected filter with ACCEPT action")
}

// That /24 should now be allowed
for _, addr := range []string{
"/ip4/1.2.3.1/tcp/123",
"/ip4/1.2.3.254/tcp/123",
"/ip4/1.2.3.254/udp/321",
} {
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
t.Error(err)
}
if f.AddrBlocked(maddr) {
t.Fatalf("expected %s to not be blocked", notBlocked)
t.Fatalf("expected %s to be whitelisted", addr)
}
}

// No policy matches these maddrs, they should be blocked by default
for _, blocked := range []string{
"/ip4/1.2.4.4/tcp/123",
"/ip4/4.3.2.1/udp/123",
"/ip6/fd00::2/tcp/321",
"/ip6/fc00::1/udp/321",
} {
maddr, err := ma.NewMultiaddr(blocked)
if err != nil {
t.Error(err)
}
if !f.AddrBlocked(maddr) {
t.Fatalf("expected %s to be blocked", blocked)
}
}
}

func TestFiltersRemoveRules(t *testing.T) {
f := NewFilters()

ips := []string{
"/ip4/1.2.3.1/tcp/123",
"/ip4/1.2.3.254/tcp/123",
}

// Add default reject filter
f.DefaultAction = ActionDeny

// Add whitelisting
_, ipnet, _ := net.ParseCIDR("1.2.3.0/24")
f.AddFilter(*ipnet, ActionAccept)

if a, ok := f.ActionForFilter(*ipnet); !ok || a != ActionAccept {
t.Fatal("Expected filter with ACCEPT action")
}

// these are all whitelisted, should be OK
for _, addr := range ips {
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
t.Error(err)
}
if f.AddrBlocked(maddr) {
t.Fatalf("expected %s to be whitelisted", addr)
}
}

// Test removing the filter. It'll remove multiple, so make a dupe &
// a complement
f.AddDialFilter(ipnet)

// Show that they all apply, these are now blacklisted & should fail
for _, addr := range ips {
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
t.Error(err)
}
if !f.AddrBlocked(maddr) {
t.Fatalf("expected %s to be blacklisted", addr)
}
}

// remove those rules
if !f.RemoveLiteral(*ipnet) {
t.Error("expected true value from RemoveLiteral")
}

// our default is reject, so the 1.2.3.0/24 should be rejected now,
// along with everything else
for _, addr := range ips {
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
t.Error(err)
}
if !f.AddrBlocked(maddr) {
t.Fatalf("expected %s to be blocked", addr)
}
}
}

0 comments on commit 0be077b

Please sign in to comment.