diff --git a/Makefile b/Makefile index 4de9c30..fa5197a 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,3 @@ -gx: - go get github.com/whyrusleeping/gx - go get github.com/whyrusleeping/gx-go - -covertools: - go get golang.org/x/tools/cmd/cover - -deps: gx covertools - gx --verbose install --global - gx-go rewrite - -publish: - gx-go rewrite --undo - conformance: tmp/multiaddr go build -o tmp/multiaddr/test/go-multiaddr ./multiaddr cd tmp/multiaddr/test && MULTIADDR_BIN="./go-multiaddr" go test -v @@ -23,4 +9,4 @@ tmp/multiaddr: clean: rm -rf tmp/ -.PHONY: gx covertools deps publish conformance clean +.PHONY: conformance clean diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..6751202 --- /dev/null +++ b/filter.go @@ -0,0 +1,179 @@ +package multiaddr + +import ( + "net" + "sync" +) + +// 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 []*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{ + 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. +// +// Deprecated: use RemoveLiteral() instead. +func (fs *Filters) Remove(ipnet *net.IPNet) (removed bool) { + return fs.RemoveLiteral(*ipnet) +} + +// 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() + + if idx, _ := fs.find(ipnet); idx != -1 { + fs.filters = append(fs.filters[:idx], fs.filters[idx+1:]...) + return true + } + return false +} + +// 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 Multiaddr) (deny bool) { + var ( + netip net.IP + found bool + ) + + ForEach(a, func(c Component) bool { + switch c.Protocol().Code { + case P_IP6ZONE: + return true + case P_IP6, P_IP4: + found = true + netip = net.IP(c.RawValue()) + return false + default: + return false + } + }) + + if !found { + return fs.DefaultAction == ActionDeny + } + + fs.mu.RLock() + defer fs.mu.RUnlock() + + action := fs.DefaultAction + for _, ft := range fs.filters { + if ft.f.Contains(netip) { + action = ft.action + } + } + + 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 { + res := res // allocate a new copy + result = append(result, &res) + } + return result +} + +func (fs *Filters) ActionForFilter(ipnet net.IPNet) (action Action, ok bool) { + if _, f := fs.find(ipnet); f != nil { + return f.action, true + } + return ActionNone, false +} + +// 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 new file mode 100644 index 0000000..741ee0a --- /dev/null +++ b/filter_test.go @@ -0,0 +1,202 @@ +package multiaddr + +import ( + "net" + "testing" +) + +func TestFilterListing(t *testing.T) { + f := NewFilters() + expected := map[string]bool{ + "1.2.3.0/24": true, + "4.3.2.1/32": true, + "fd00::/8": true, + "fc00::1/128": true, + } + for cidr := range expected { + _, ipnet, _ := net.ParseCIDR(cidr) + f.AddDialFilter(ipnet) + } + + for _, filter := range f.Filters() { + cidr := filter.String() + if expected[cidr] { + delete(expected, cidr) + } else { + t.Errorf("unexected filter %s", cidr) + } + } + for cidr := range expected { + t.Errorf("expected filter %s", cidr) + } +} + +func TestFilterBlocking(t *testing.T) { + f := NewFilters() + + _, ipnet, _ := net.ParseCIDR("0.1.2.3/24") + f.AddDialFilter(ipnet) + filters := f.Filters() + if len(filters) != 1 { + t.Fatal("Expected only 1 filter") + } + + 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", + "4.3.2.1/32", + "fd00::/8", + "fc00::1/128", + } { + _, ipnet, _ := net.ParseCIDR(cidr) + 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", + "/ip6/fd00::2/tcp/321", + "/ip6/fc00::1/udp/321", + } { + maddr, err := NewMultiaddr(blocked) + if err != nil { + t.Error(err) + } + if !f.AddrBlocked(maddr) { + t.Fatalf("expected %s to be blocked", blocked) + } + } + + // 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 := 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 := NewMultiaddr(addr) + if err != nil { + t.Error(err) + } + if f.AddrBlocked(maddr) { + 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 := 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 := 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 := 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 := NewMultiaddr(addr) + if err != nil { + t.Error(err) + } + if !f.AddrBlocked(maddr) { + t.Fatalf("expected %s to be blocked", addr) + } + } +} diff --git a/go.mod b/go.mod index 76696f8..a340eda 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/multiformats/go-multiaddr +go 1.13 + require ( github.com/multiformats/go-multihash v0.0.13 - github.com/multiformats/go-varint v0.0.2 + github.com/multiformats/go-varint v0.0.5 ) - -go 1.13 diff --git a/go.sum b/go.sum index 48d1b41..b36340c 100644 --- a/go.sum +++ b/go.sum @@ -2,20 +2,10 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 h1:MHkK1uRtFbVqvAgvWxafZe54+5uBxLluGylDiKgdhwo= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/mr-tron/base58 v1.1.2 h1:ZEw4I2EgPKDJ2iEw0cNmLB3ROrEmkOtXIkaG7wZg+78= -github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/multiformats/go-multihash v0.0.8 h1:wrYcW5yxSi3dU07n5jnuS5PrNwyHy0zRHGVoUugWvXg= -github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= -github.com/multiformats/go-multihash v0.0.10 h1:lMoNbh2Ssd9PUF74Nz008KGzGPlfeV6wH3rit5IIGCM= -github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc= github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-varint v0.0.1 h1:TR/0rdQtnNxuN2IhiB639xC3tWM4IUi7DkTBVTdGW/M= -github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/multiformats/go-varint v0.0.2 h1:6sUvyh2YHpJCb8RZ6eYzj6iJQ4+chWYmyIHxszqlPTA= -github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=