From dd0cde1cfd8d2ceddd3db7075bac915c1c7b21bf Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 9 Jun 2022 22:26:24 -0700 Subject: [PATCH] Add allowlist impl Accepts multiaddrs that define who is allowed. --- allowlist.go | 230 ++++++++++++++++++++++++++++++++++++++++++++++ allowlist_test.go | 220 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 allowlist.go create mode 100644 allowlist_test.go diff --git a/allowlist.go b/allowlist.go new file mode 100644 index 0000000..8f38806 --- /dev/null +++ b/allowlist.go @@ -0,0 +1,230 @@ +package rcmgr + +import ( + "bytes" + "errors" + "net" + + "github.com/libp2p/go-libp2p-core/peer" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" +) + +type allowlist struct { + // TODO do we want to make this lookup faster? + + // Any peer with these IPs are allowed + allowedIPs []net.IP + allowedNetworks []*net.IPNet + + // Only the specified peers can use these IPs + allowedPeerByIP map[peer.ID][]net.IP + allowedPeerByNetwork map[peer.ID][]*net.IPNet +} + +func newAllowList() allowlist { + return allowlist{ + allowedPeerByIP: make(map[peer.ID][]net.IP), + allowedPeerByNetwork: make(map[peer.ID][]*net.IPNet), + } +} + +func toIPOrIPNet(ma multiaddr.Multiaddr) (net.IP, *net.IPNet, string, error) { + var ipString string + var mask string + var allowedPeer string + + multiaddr.ForEach(ma, func(c multiaddr.Component) bool { + if c.Protocol().Code == multiaddr.P_IP4 || c.Protocol().Code == multiaddr.P_IP6 { + ipString = c.Value() + } + if c.Protocol().Code == multiaddr.P_IPCIDR { + mask = c.Value() + } + if c.Protocol().Code == multiaddr.P_P2P { + allowedPeer = c.Value() + } + return ipString == "" || mask == "" || allowedPeer == "" + }) + + if ipString == "" { + return nil, nil, allowedPeer, errors.New("missing ip address") + } + + if mask == "" { + ip := net.ParseIP(ipString) + if ip == nil { + return nil, nil, allowedPeer, errors.New("invalid ip address") + } + return ip, nil, allowedPeer, nil + } + + _, ipnet, err := net.ParseCIDR(ipString + "/" + mask) + return nil, ipnet, allowedPeer, err + +} + +func (al *allowlist) Add(ma multiaddr.Multiaddr) error { + ip, ipnet, allowedPeerStr, err := toIPOrIPNet(ma) + if err != nil { + return err + } + + if allowedPeerStr != "" { + // We have a peerID constraint + allowedPeer, err := peer.Decode(allowedPeerStr) + if err != nil { + return err + } + if ip != nil { + al.allowedPeerByIP[allowedPeer] = append(al.allowedPeerByIP[allowedPeer], ip) + } else if ipnet != nil { + al.allowedPeerByNetwork[allowedPeer] = append(al.allowedPeerByNetwork[allowedPeer], ipnet) + } + } else { + if ip != nil { + al.allowedIPs = append(al.allowedIPs, ip) + } else if ipnet != nil { + al.allowedNetworks = append(al.allowedNetworks, ipnet) + } + } + return nil +} + +func (al *allowlist) Remove(ma multiaddr.Multiaddr) error { + ip, ipnet, allowedPeerStr, err := toIPOrIPNet(ma) + if err != nil { + return err + } + ipList := al.allowedIPs + ipNetList := al.allowedNetworks + + var allowedPeer peer.ID + if allowedPeerStr != "" { + // We have a peerID constraint + allowedPeer, err = peer.Decode(allowedPeerStr) + if err != nil { + return err + } + ipList = al.allowedPeerByIP[allowedPeer] + ipNetList = al.allowedPeerByNetwork[allowedPeer] + } + + if ip != nil { + i := len(ipList) + for i > 0 { + i-- + if ipList[i].Equal(ip) { + if i == len(ipList)-1 { + // Trim this element from the end + ipList = ipList[:i] + } else { + // swap remove + ipList[i] = ipList[len(ipList)-1] + ipList = ipList[:len(ipList)-1] + } + } + } + } else if ipnet != nil { + i := len(ipNetList) + for i > 0 { + i-- + if ipNetList[i].IP.Equal(ipnet.IP) && bytes.Equal(ipNetList[i].Mask, ipnet.Mask) { + if i == len(ipNetList)-1 { + // Trim this element from the end + ipNetList = ipNetList[:i] + } else { + // swap remove + ipNetList[i] = ipNetList[len(ipNetList)-1] + ipNetList = ipNetList[:len(ipNetList)-1] + } + } + } + } + + if allowedPeer != "" { + al.allowedPeerByIP[allowedPeer] = ipList + al.allowedPeerByNetwork[allowedPeer] = ipNetList + } else { + al.allowedIPs = ipList + al.allowedNetworks = ipNetList + } + + return nil +} + +func (al *allowlist) Allowed(ma multiaddr.Multiaddr) bool { + ip, err := manet.ToIP(ma) + if err != nil { + return false + } + + for _, allowedIP := range al.allowedIPs { + if allowedIP.Equal(ip) { + return true + } + } + + for _, network := range al.allowedNetworks { + if network.Contains(ip) { + return true + } + } + + for _, allowedIPs := range al.allowedPeerByIP { + for _, allowedIP := range allowedIPs { + if allowedIP.Equal(ip) { + return true + } + } + } + + for _, allowedNetworks := range al.allowedPeerByNetwork { + for _, network := range allowedNetworks { + if network.Contains(ip) { + return true + } + } + } + + return false +} + +func (al *allowlist) AllowedPeerAndMultiaddr(peerID peer.ID, ma multiaddr.Multiaddr) bool { + ip, err := manet.ToIP(ma) + if err != nil { + return false + } + + for _, allowedIP := range al.allowedIPs { + if allowedIP.Equal(ip) { + // We found a match that isn't constrained by a peerID + return true + } + } + + for _, network := range al.allowedNetworks { + if network.Contains(ip) { + // We found a match that isn't constrained by a peerID + return true + } + } + + if expectedIPs, ok := al.allowedPeerByIP[peerID]; ok { + for _, expectedIP := range expectedIPs { + if expectedIP.Equal(ip) { + return true + } + } + } + + if expectedNetworks, ok := al.allowedPeerByNetwork[peerID]; ok { + for _, expectedNetwork := range expectedNetworks { + if expectedNetwork.Contains(ip) { + return true + } + } + } + + return false +} diff --git a/allowlist_test.go b/allowlist_test.go new file mode 100644 index 0000000..2974644 --- /dev/null +++ b/allowlist_test.go @@ -0,0 +1,220 @@ +package rcmgr + +import ( + "fmt" + "testing" + + "github.com/libp2p/go-libp2p-core/peer" + "github.com/libp2p/go-libp2p-core/test" + "github.com/multiformats/go-multiaddr" +) + +func TestAllowed(t *testing.T) { + allowlist := newAllowList() + ma, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4/tcp/1234") + err := allowlist.Add(ma) + if err != nil { + t.Fatalf("failed to add ip4: %s", err) + } + + if !allowlist.Allowed(ma) { + t.Fatalf("addr should be allowed") + } +} + +func TestAllowedNetwork(t *testing.T) { + allowlist := newAllowList() + ma, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.0/ipcidr/24") + err := allowlist.Add(ma) + if err != nil { + t.Fatalf("failed to add ip4: %s", err) + } + + ma2, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.20/tcp/1234") + if !allowlist.Allowed(ma2) { + t.Fatalf("addr should be allowed") + } +} + +func TestAllowedPeerOnIP(t *testing.T) { + allowlist := newAllowList() + p, err := test.RandPeerID() + if err != nil { + t.Fatalf("failed to gen peer ip4: %s", err) + } + + ma, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4/p2p/" + peer.Encode(p)) + err = allowlist.Add(ma) + if err != nil { + t.Fatalf("failed to add ip4: %s", err) + } + + ma2, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4") + if !allowlist.AllowedPeerAndMultiaddr(p, ma2) { + t.Fatalf("addr should be allowed") + } +} + +func TestAllowedPeerOnNetwork(t *testing.T) { + allowlist := newAllowList() + p, err := test.RandPeerID() + if err != nil { + t.Fatalf("failed to gen peer ip4: %s", err) + } + + ma, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.0/ipcidr/24/p2p/" + peer.Encode(p)) + err = allowlist.Add(ma) + if err != nil { + t.Fatalf("failed to add ip4: %s", err) + } + + ma2, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4") + if !allowlist.AllowedPeerAndMultiaddr(p, ma2) { + t.Fatalf("addr should be allowed") + } +} + +func TestAllowedWithPeer(t *testing.T) { + type testcase struct { + name string + allowlist []string + endpoint multiaddr.Multiaddr + peer peer.ID + isAllowed bool + } + + peerA := test.RandPeerIDFatal(t) + peerB := test.RandPeerIDFatal(t) + multiaddrA, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4/tcp/1234") + multiaddrB, _ := multiaddr.NewMultiaddr("/ip4/2.2.3.4/tcp/1234") + + testcases := []testcase{ + { + name: "Blocked", + isAllowed: false, + allowlist: []string{"/ip4/1.2.3.1"}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "Blocked wrong peer", + isAllowed: false, + allowlist: []string{"/ip4/1.2.3.4" + "/p2p/" + peer.Encode(peerB)}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "allowed on network", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.0/ipcidr/24"}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "Blocked peer not on network", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.0/ipcidr/24"}, + endpoint: multiaddrA, + peer: peerA, + }, { + name: "allowed. right network, right peer", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.0/ipcidr/24" + "/p2p/" + peer.Encode(peerA)}, + endpoint: multiaddrA, + peer: peerA, + }, { + name: "allowed. right network, no peer", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.0/ipcidr/24"}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "Blocked. right network, wrong peer", + isAllowed: false, + allowlist: []string{"/ip4/1.2.3.0/ipcidr/24" + "/p2p/" + peer.Encode(peerB)}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "allowed peer any ip", + isAllowed: true, + allowlist: []string{"/ip4/0.0.0.0/ipcidr/0"}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "allowed peer multiple ips in allowlist", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.4/p2p/" + peer.Encode(peerA), "/ip4/2.2.3.4/p2p/" + peer.Encode(peerA)}, + endpoint: multiaddrA, + peer: peerA, + }, + { + name: "allowed peer multiple ips in allowlist", + isAllowed: true, + allowlist: []string{"/ip4/1.2.3.4/p2p/" + peer.Encode(peerA), "/ip4/2.2.3.4/p2p/" + peer.Encode(peerA)}, + endpoint: multiaddrB, + peer: peerA, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + allowlist := newAllowList() + for _, maStr := range tc.allowlist { + ma, err := multiaddr.NewMultiaddr(maStr) + if err != nil { + fmt.Printf("failed to parse multiaddr: %s", err) + } + allowlist.Add(ma) + } + + if allowlist.AllowedPeerAndMultiaddr(tc.peer, tc.endpoint) != tc.isAllowed { + t.Fatalf("%v: expected %v", !tc.isAllowed, tc.isAllowed) + } + }) + } + +} + +func TestRemoved(t *testing.T) { + type testCase struct { + name string + allowedMA string + } + peerA := test.RandPeerIDFatal(t) + maA, _ := multiaddr.NewMultiaddr("/ip4/1.2.3.4") + + testCases := []testCase{ + {name: "ip4", allowedMA: "/ip4/1.2.3.4"}, + {name: "ip4 with peer", allowedMA: "/ip4/1.2.3.4/p2p/" + peer.Encode(peerA)}, + {name: "ip4 network", allowedMA: "/ip4/0.0.0.0/ipcidr/0"}, + {name: "ip4 network with peer", allowedMA: "/ip4/0.0.0.0/ipcidr/0/p2p/" + peer.Encode(peerA)}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + allowlist := newAllowList() + ma, err := multiaddr.NewMultiaddr(tc.allowedMA) + if err != nil { + t.Fatalf("failed to parse ma: %s", err) + } + + err = allowlist.Add(ma) + if err != nil { + t.Fatalf("failed to add ip4: %s", err) + } + + if !allowlist.AllowedPeerAndMultiaddr(peerA, maA) { + t.Fatalf("addr should be allowed") + } + + allowlist.Remove((ma)) + + if allowlist.AllowedPeerAndMultiaddr(peerA, maA) { + t.Fatalf("addr should not be allowed") + } + }) + } +}