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

Implement support for whitelists, default-deny/allow (rebase) #8

Merged
merged 5 commits into from
May 10, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,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
148 changes: 123 additions & 25 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,158 @@ 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) {
raulk marked this conversation as resolved.
Show resolved Hide resolved
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})
}
raulk marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
ft.f.Mask.Size()
raulk marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
}