diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 321728888..2115ed08f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -282,7 +282,7 @@ }, { "ImportPath": "github.com/whyrusleeping/multiaddr-filter", - "Rev": "15837fcc356fddef27c634b0f6379b3b7f259114" + "Rev": "9e26222151125ecd3fc1fd190179b6bdd55f5608" }, { "ImportPath": "golang.org/x/crypto/blowfish", diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/LICENSE b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/LICENSE new file mode 100644 index 000000000..1a976e72e --- /dev/null +++ b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jeromy Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/README.md b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/README.md new file mode 100644 index 000000000..6a3ca8f83 --- /dev/null +++ b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/README.md @@ -0,0 +1,15 @@ +# go-multiaddr-filter -- CIDR netmasks with multiaddr + +This module creates very simple [multiaddr](https://github.com/jbenet/go-multiaddr) formatted cidr netmasks. + +It doesn't do full multiaddr parsing to save on vendoring things and perf. The `net` package will take care of verifying the validity of the network part anyway. + +## Usage + +```go + +import filter "github.com/whyrusleeping/multiaddr-filter" + +filter.NewMask("/ip4/192.168.0.0/24") // ipv4 +filter.NewMask("/ip6/fe80::/64") // ipv6 +``` diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask.go b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask.go index 91208fe04..dec913d76 100644 --- a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask.go +++ b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask.go @@ -2,18 +2,46 @@ package mask import ( "errors" + "fmt" "net" "strings" + + manet "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr-net" ) +var ErrInvalidFormat = errors.New("invalid multiaddr-filter format") + func NewMask(a string) (*net.IPNet, error) { parts := strings.Split(a, "/") - if len(parts) == 5 && parts[1] == "ip4" && parts[3] == "ipcidr" { - _, ipn, err := net.ParseCIDR(parts[2] + "/" + parts[4]) - if err != nil { - return nil, err - } - return ipn, nil + + if parts[0] != "" { + return nil, ErrInvalidFormat } - return nil, errors.New("invalid format") + + if len(parts) != 5 { + return nil, ErrInvalidFormat + } + + // check it's a valid filter address. ip + cidr + isip := parts[1] == "ip4" || parts[1] == "ip6" + iscidr := parts[3] == "ipcidr" + if !isip || !iscidr { + return nil, ErrInvalidFormat + } + + _, ipn, err := net.ParseCIDR(parts[2] + "/" + parts[4]) + if err != nil { + return nil, err + } + return ipn, nil +} + +func ConvertIPNet(n *net.IPNet) (string, error) { + addr, err := manet.FromIP(n.IP) + if err != nil { + return "", err + } + + b, _ := n.Mask.Size() + return fmt.Sprintf("%s/ipcidr/%d", addr, b), nil } diff --git a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask_test.go b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask_test.go index 4507dca68..2dcc016b1 100644 --- a/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask_test.go +++ b/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter/mask_test.go @@ -5,6 +5,74 @@ import ( "testing" ) +func TestValidMasks(t *testing.T) { + + cidrOrFatal := func(s string) *net.IPNet { + _, ipn, err := net.ParseCIDR(s) + if err != nil { + t.Fatal(err) + } + return ipn + } + + testCases := map[string]*net.IPNet{ + "/ip4/1.2.3.4/ipcidr/0": cidrOrFatal("1.2.3.4/0"), + "/ip4/1.2.3.4/ipcidr/32": cidrOrFatal("1.2.3.4/32"), + "/ip4/1.2.3.4/ipcidr/24": cidrOrFatal("1.2.3.4/24"), + "/ip4/192.168.0.0/ipcidr/28": cidrOrFatal("192.168.0.0/28"), + "/ip6/fe80::/ipcidr/0": cidrOrFatal("fe80::/0"), + "/ip6/fe80::/ipcidr/64": cidrOrFatal("fe80::/64"), + "/ip6/fe80::/ipcidr/128": cidrOrFatal("fe80::/128"), + } + + for s, m1 := range testCases { + m2, err := NewMask(s) + if err != nil { + t.Error("should be invalid:", s) + continue + } + + if m1.String() != m2.String() { + t.Error("masks not equal:", m1, m2) + } + } + +} + +func TestInvalidMasks(t *testing.T) { + + testCases := []string{ + "/", + "/ip4/10.1.2.3", + "/ip6/::", + "/ip4/1.2.3.4/cidr/24", + "/ip6/fe80::/cidr/24", + "/eth/aa:aa:aa:aa:aa/ipcidr/24", + "foobar/ip4/1.2.3.4/ipcidr/32", + } + + for _, s := range testCases { + _, err := NewMask(s) + if err != ErrInvalidFormat { + t.Error("should be invalid:", s) + } + } + + testCases2 := []string{ + "/ip4/1.2.3.4/ipcidr/33", + "/ip4/192.168.0.0/ipcidr/-1", + "/ip6/fe80::/ipcidr/129", + } + + for _, s := range testCases2 { + _, err := NewMask(s) + if err == nil { + t.Error("should be invalid:", s) + } + } + +} + func TestFiltered(t *testing.T) { var tests = map[string]map[string]bool{ "/ip4/10.0.0.0/ipcidr/8": map[string]bool{ @@ -34,3 +102,31 @@ func TestFiltered(t *testing.T) { } } } + +func TestParsing(t *testing.T) { + var addrs = map[string]string{ + "/ip4/192.168.0.0/ipcidr/16": "192.168.0.0/16", + "/ip4/192.0.0.0/ipcidr/8": "192.0.0.0/8", + "/ip6/2001:db8::/ipcidr/32": "2001:db8::/32", + } + + for k, v := range addrs { + m, err := NewMask(k) + if err != nil { + t.Fatal(err) + } + + if m.String() != v { + t.Fatalf("mask is wrong: ", m, v) + } + + orig, err := ConvertIPNet(m) + if err != nil { + t.Fatal(err) + } + + if orig != k { + t.Fatal("backwards conversion failed: ", orig, k) + } + } +} diff --git a/core/commands/swarm.go b/core/commands/swarm.go index efb6fc1e7..52efa12ed 100644 --- a/core/commands/swarm.go +++ b/core/commands/swarm.go @@ -9,10 +9,12 @@ import ( "sort" cmds "github.com/ipfs/go-ipfs/commands" + swarm "github.com/ipfs/go-ipfs/p2p/net/swarm" peer "github.com/ipfs/go-ipfs/p2p/peer" iaddr "github.com/ipfs/go-ipfs/util/ipfsaddr" ma "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" + mafilter "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/whyrusleeping/multiaddr-filter" context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" ) @@ -32,6 +34,7 @@ ipfs swarm peers - List peers with open connections ipfs swarm addrs - List known addresses. Useful to debug. ipfs swarm connect
- Open connection to a given address ipfs swarm disconnect
- Close connection to a given address +ipfs swarm filters - Manipulate filters addresses `, ShortDescription: ` ipfs swarm is a tool to manipulate the network swarm. The swarm is the @@ -44,6 +47,7 @@ ipfs peers in the internet. "addrs": swarmAddrsCmd, "connect": swarmConnectCmd, "disconnect": swarmDisconnectCmd, + "filters": swarmFiltersCmd, }, } @@ -358,3 +362,142 @@ func peersWithAddresses(addrs []string) (pis []peer.PeerInfo, err error) { } return pis, nil } + +var swarmFiltersCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Manipulate address filters", + ShortDescription: ` +'ipfs swarm filters' will list out currently applied filters. Its subcommands can be used +to add or remove said filters. Filters are specified using the multiaddr-filter format: + +example: + + /ip4/192.168.0.0/ipcidr/16 + +Where the above is equivalent to the standard CIDR: + + 192.168.0.0/16 + +Filters default to those specified under the "DialBlocklist" config key. +`, + }, + Subcommands: map[string]*cmds.Command{ + "add": swarmFiltersAddCmd, + "rm": swarmFiltersRmCmd, + }, + Run: func(req cmds.Request, res cmds.Response) { + n, err := req.Context().GetNode() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + snet, ok := n.PeerHost.Network().(*swarm.Network) + if !ok { + res.SetError(errors.New("failed to cast network to swarm network"), cmds.ErrNormal) + return + } + + var output []string + for _, f := range snet.Filters.Filters() { + s, err := mafilter.ConvertIPNet(f) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + output = append(output, s) + } + res.SetOutput(&stringList{output}) + }, + Marshalers: cmds.MarshalerMap{ + cmds.Text: stringListMarshaler, + }, + Type: stringList{}, +} + +var swarmFiltersAddCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "add an address filter", + ShortDescription: ` +'ipfs swarm filters add' will add an address filter to the daemons swarm. +Filters applied this way will not persist daemon reboots, to acheive that, +add your filters to the ipfs config file. +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("address", true, true, "multiaddr to filter").EnableStdin(), + }, + Run: func(req cmds.Request, res cmds.Response) { + n, err := req.Context().GetNode() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + snet, ok := n.PeerHost.Network().(*swarm.Network) + if !ok { + res.SetError(errors.New("failed to cast network to swarm network"), cmds.ErrNormal) + return + } + + for _, arg := range req.Arguments() { + mask, err := mafilter.NewMask(arg) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + snet.Filters.AddDialFilter(mask) + } + }, +} + +var swarmFiltersRmCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "remove an address filter", + ShortDescription: ` +'ipfs swarm filters rm' will remove an address filter from the daemons swarm. +Filters removed this way will not persist daemon reboots, to acheive that, +remove your filters from the ipfs config file. +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("address", true, true, "multiaddr filter to remove").EnableStdin(), + }, + Run: func(req cmds.Request, res cmds.Response) { + n, err := req.Context().GetNode() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + if n.PeerHost == nil { + res.SetError(errNotOnline, cmds.ErrNormal) + return + } + + snet, ok := n.PeerHost.Network().(*swarm.Network) + if !ok { + res.SetError(errors.New("failed to cast network to swarm network"), cmds.ErrNormal) + return + } + + if req.Arguments()[0] == "all" || req.Arguments()[0] == "*" { + fs := snet.Filters.Filters() + for _, f := range fs { + snet.Filters.Remove(f) + } + return + } + + for _, arg := range req.Arguments() { + mask, err := mafilter.NewMask(arg) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + snet.Filters.Remove(mask) + } + }, +} diff --git a/p2p/net/filter/filter.go b/p2p/net/filter/filter.go index 1642a8b77..21127d3f7 100644 --- a/p2p/net/filter/filter.go +++ b/p2p/net/filter/filter.go @@ -9,11 +9,17 @@ import ( ) type Filters struct { - filters []*net.IPNet + filters map[string]*net.IPNet +} + +func NewFilters() *Filters { + return &Filters{ + filters: make(map[string]*net.IPNet), + } } func (fs *Filters) AddDialFilter(f *net.IPNet) { - fs.filters = append(fs.filters, f) + fs.filters[f.String()] = f } func (f *Filters) AddrBlocked(a ma.Multiaddr) bool { @@ -32,3 +38,15 @@ func (f *Filters) AddrBlocked(a ma.Multiaddr) bool { } return false } + +func (f *Filters) Filters() []*net.IPNet { + var out []*net.IPNet + for _, ff := range f.filters { + out = append(out, ff) + } + return out +} + +func (f *Filters) Remove(ff *net.IPNet) { + delete(f.filters, ff.String()) +} diff --git a/p2p/net/swarm/swarm.go b/p2p/net/swarm/swarm.go index 9898f1551..c3e9f2f80 100644 --- a/p2p/net/swarm/swarm.go +++ b/p2p/net/swarm/swarm.go @@ -84,7 +84,7 @@ func NewSwarm(ctx context.Context, listenAddrs []ma.Multiaddr, dialT: DialTimeout, notifs: make(map[inet.Notifiee]ps.Notifiee), bwc: bwc, - Filters: new(filter.Filters), + Filters: filter.NewFilters(), } // configure Swarm diff --git a/test/sharness/t0141-addfilter.sh b/test/sharness/t0141-addfilter.sh new file mode 100755 index 000000000..43ade6743 --- /dev/null +++ b/test/sharness/t0141-addfilter.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# +# Copyright (c) 2014 Jeromy Johnson +# MIT Licensed; see the LICENSE file in this repository. +# + +test_description="Test ipfs swarm command" + +AF1="/ip4/192.168.0.0/ipcidr/16" +AF2="/ip4/127.0.0.0/ipcidr/8" +AF3="/ip6/2008:bcd::/ipcidr/32" +AF4="/ip4/172.16.0.0/ipcidr/12" + +. lib/test-lib.sh + +test_init_ipfs + +test_swarm_filter_cmd() { + printf "" > list_expected + for AF in "$@" + do + echo "$AF" >>list_expected + done + + test_expect_success "'ipfs swarm filters' succeeds" ' + ipfs swarm filters > list_actual + ' + + test_expect_success "'ipfs swarm filters' output looks good" ' + test_sort_cmp list_actual list_expected + ' +} + +test_swarm_filters() { + + ipfs swarm filters rm all + + test_swarm_filter_cmd + + test_expect_success "'ipfs swarm filter add' succeeds" ' + ipfs swarm filters add $AF1 $AF2 $AF3 + ' + + test_swarm_filter_cmd $AF1 $AF2 $AF3 + + test_expect_success "'ipfs swarm filter rm' succeeds" ' + ipfs swarm filters rm $AF2 $AF3 + ' + + test_swarm_filter_cmd $AF1 + + test_expect_success "'ipfs swarm filter add' succeeds" ' + ipfs swarm filters add $AF4 $AF2 + ' + + test_swarm_filter_cmd $AF1 $AF2 $AF4 + + test_expect_success "'ipfs swarm filter rm' succeeds" ' + ipfs swarm filters rm $AF1 $AF2 $AF4 + ' + + test_swarm_filter_cmd +} + +test_launch_ipfs_daemon + +test_swarm_filters + +test_kill_ipfs_daemon + +test_done