diff --git a/README.md b/README.md index f6787a2..4c20e3b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/filter.go b/filter.go index 88f9792..77c4867 100644 --- a/filter.go +++ b/filter.go @@ -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 } diff --git a/filter_test.go b/filter_test.go index ae33847..8b5a08c 100644 --- a/filter_test.go +++ b/filter_test.go @@ -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") @@ -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", @@ -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", @@ -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) } } }