Repository: libp2p/go-libp2p
Pull request #2807 (2024-05-21 21:48:56)

rcmgr: Support specific network prefix in conn limiter
Despite the field being called "ConnLimitPerCIDR" you weren't able to define limits for a specific CIDR address block. I've renamed this to ConnLimitPerSubnet to make it clearer that this connection limit applies to each subnet with the same N bit prefix.

I've also added an option to allow defining limits on specific address prefix blocks. If an IP address matches one of these blocks then that limit is used instead of the more generic `ConnLimitPerSubnet`. By default you are allowed unlimited connections from localhost. This should prevent test breakages we've seen.

This does break the existing API. I think that's okay because:
- We just released this API, so hopefully no one is using it yet. Or, if they are, it's fresh on their mind.
- Most users probably just stuck with the defaults. And the defaults here are better.
- The old names were confusing at best.


Patch:
diff --git a/p2p/host/resource-manager/conn_limiter.go b/p2p/host/resource-manager/conn_limiter.go
index de201d12..717249e8 100644
--- a/p2p/host/resource-manager/conn_limiter.go
+++ b/p2p/host/resource-manager/conn_limiter.go
@@ -1,13 +1,26 @@
 package rcmgr
 
 import (
+	"math"
 	"net/netip"
+	"slices"
 	"sync"
 )
 
-type ConnLimitPerCIDR struct {
-	// How many leading 1 bits in the mask
-	BitMask   int
+type ConnLimitPerSubnet struct {
+	// This defines how big the subnet is. For example, a /24 subnet has a
+	// PrefixLength of 24. All IPs that share the same 24 bit prefix are in the same
+	// subnet. Are in the same subnet, and bound to the same limit.
+	PrefixLength int
+	// The maximum number of connections allowed for each subnet.
+	ConnCount int
+}
+
+type NetworkPrefixLimit struct {
+	// The Network prefix for which this limit applies.
+	Network netip.Prefix
+
+	// The maximum number of connections allowed for this subnet.
 	ConnCount int
 }
 
@@ -16,45 +29,114 @@ type ConnLimitPerCIDR struct {
 // down
 var defaultMaxConcurrentConns = 8
 
-var defaultIP4Limit = ConnLimitPerCIDR{
-	ConnCount: defaultMaxConcurrentConns,
-	BitMask:   32,
+var defaultIP4Limit = ConnLimitPerSubnet{
+	ConnCount:    defaultMaxConcurrentConns,
+	PrefixLength: 32,
 }
-var defaultIP6Limits = []ConnLimitPerCIDR{
+var defaultIP6Limits = []ConnLimitPerSubnet{
 	{
-		ConnCount: defaultMaxConcurrentConns,
-		BitMask:   56,
+		ConnCount:    defaultMaxConcurrentConns,
+		PrefixLength: 56,
 	},
 	{
-		ConnCount: 8 * defaultMaxConcurrentConns,
-		BitMask:   48,
+		ConnCount:    8 * defaultMaxConcurrentConns,
+		PrefixLength: 48,
 	},
 }
 
-func WithLimitPeersPerCIDR(ipv4 []ConnLimitPerCIDR, ipv6 []ConnLimitPerCIDR) Option {
+var DefaultNetworkPrefixLimitV4 = sortNetworkPrefixes([]NetworkPrefixLimit{
+	{
+		// Loopback address for v4 https://datatracker.ietf.org/doc/html/rfc6890#section-2.2.2
+		Network:   netip.MustParsePrefix("127.0.0.0/8"),
+		ConnCount: math.MaxInt, // Unlimited
+	},
+})
+var DefaultNetworkPrefixLimitV6 = sortNetworkPrefixes([]NetworkPrefixLimit{
+	{
+		// Loopback address for v6 https://datatracker.ietf.org/doc/html/rfc6890#section-2.2.3
+		Network:   netip.MustParsePrefix("::1/128"),
+		ConnCount: math.MaxInt, // Unlimited
+	},
+})
+
+// Network prefixes limits must be sorted by most specific to least specific.  This lets us
+// actually use the more specific limits, otherwise only the less specific ones
+// would be matched. e.g. 1.2.3.0/24 must come before 1.2.0.0/16.
+func sortNetworkPrefixes(limits []NetworkPrefixLimit) []NetworkPrefixLimit {
+	slices.SortStableFunc(limits, func(a, b NetworkPrefixLimit) int {
+		return b.Network.Bits() - a.Network.Bits()
+	})
+	return limits
+}
+
+// WithNetworkPrefixLimit sets the limits for the number of connections allowed
+// for a specific Network Prefix. Use this when you want to set higher limits
+// for a specific subnet than the default limit per subnet.
+func WithNetworkPrefixLimit(ipv4 []NetworkPrefixLimit, ipv6 []NetworkPrefixLimit) Option {
+	return func(rm *resourceManager) error {
+		if ipv4 != nil {
+			rm.connLimiter.networkPrefixLimitV4 = sortNetworkPrefixes(ipv4)
+		}
+		if ipv6 != nil {
+			rm.connLimiter.networkPrefixLimitV6 = sortNetworkPrefixes(ipv6)
+		}
+		return nil
+	}
+}
+
+// WithLimitPerSubnet sets the limits for the number of connections allowed per
+// subnet. This will limit the number of connections per subnet if that subnet
+// is not defined in the NetworkPrefixLimit option. Think of this as a default
+// limit for any given subnet.
+func WithLimitPerSubnet(ipv4 []ConnLimitPerSubnet, ipv6 []ConnLimitPerSubnet) Option {
 	return func(rm *resourceManager) error {
 		if ipv4 != nil {
-			rm.connLimiter.connLimitPerCIDRIP4 = ipv4
+			rm.connLimiter.connLimitPerSubnetV4 = ipv4
 		}
 		if ipv6 != nil {
-			rm.connLimiter.connLimitPerCIDRIP6 = ipv6
+			rm.connLimiter.connLimitPerSubnetV6 = ipv6
 		}
 		return nil
 	}
 }
 
 type connLimiter struct {
-	mu                  sync.Mutex
-	connLimitPerCIDRIP4 []ConnLimitPerCIDR
-	connLimitPerCIDRIP6 []ConnLimitPerCIDR
-	ip4connsPerLimit    []map[string]int
-	ip6connsPerLimit    []map[string]int
+	mu sync.Mutex
+
+	// Specific Network Prefix limits. If these are set, they take precedence over the
+	// subnet limits.
+	// These must be sorted by most specific to least specific.
+	networkPrefixLimitV4    []NetworkPrefixLimit
+	networkPrefixLimitV6    []NetworkPrefixLimit
+	connsPerNetworkPrefixV4 []int
+	connsPerNetworkPrefixV6 []int
+
+	// Subnet limits.
+	connLimitPerSubnetV4 []ConnLimitPerSubnet
+	connLimitPerSubnetV6 []ConnLimitPerSubnet
+	ip4connsPerLimit     []map[string]int
+	ip6connsPerLimit     []map[string]int
 }
 
 func newConnLimiter() *connLimiter {
 	return &connLimiter{
-		connLimitPerCIDRIP4: []ConnLimitPerCIDR{defaultIP4Limit},
-		connLimitPerCIDRIP6: defaultIP6Limits,
+		networkPrefixLimitV4: DefaultNetworkPrefixLimitV4,
+		networkPrefixLimitV6: DefaultNetworkPrefixLimitV6,
+
+		connLimitPerSubnetV4: []ConnLimitPerSubnet{defaultIP4Limit},
+		connLimitPerSubnetV6: defaultIP6Limits,
+	}
+}
+
+func (cl *connLimiter) addNetworkPrefixLimit(isIP6 bool, npLimit NetworkPrefixLimit) {
+	cl.mu.Lock()
+	defer cl.mu.Unlock()
+	if isIP6 {
+		cl.networkPrefixLimitV6 = append(cl.networkPrefixLimitV6, npLimit)
+		cl.networkPrefixLimitV6 = sortNetworkPrefixes(cl.networkPrefixLimitV6)
+	} else {
+		cl.networkPrefixLimitV4 = append(cl.networkPrefixLimitV4, npLimit)
+		cl.networkPrefixLimitV4 = sortNetworkPrefixes(cl.networkPrefixLimitV4)
 	}
 }
 
@@ -62,35 +144,62 @@ func newConnLimiter() *connLimiter {
 func (cl *connLimiter) addConn(ip netip.Addr) bool {
 	cl.mu.Lock()
 	defer cl.mu.Unlock()
-	limits := cl.connLimitPerCIDRIP4
-	countsPerLimit := cl.ip4connsPerLimit
+	networkPrefixLimits := cl.networkPrefixLimitV4
+	connsPerNetworkPrefix := cl.connsPerNetworkPrefixV4
+	limits := cl.connLimitPerSubnetV4
+	connsPerLimit := cl.ip4connsPerLimit
 	isIP6 := ip.Is6()
 	if isIP6 {
-		limits = cl.connLimitPerCIDRIP6
-		countsPerLimit = cl.ip6connsPerLimit
+		networkPrefixLimits = cl.networkPrefixLimitV6
+		connsPerNetworkPrefix = cl.connsPerNetworkPrefixV6
+		limits = cl.connLimitPerSubnetV6
+		connsPerLimit = cl.ip6connsPerLimit
 	}
 
-	if len(countsPerLimit) == 0 && len(limits) > 0 {
-		countsPerLimit = make([]map[string]int, len(limits))
+	// Check Network Prefix limits first
+	if len(connsPerNetworkPrefix) == 0 && len(networkPrefixLimits) > 0 {
+		// Initialize the counts
+		connsPerNetworkPrefix = make([]int, len(networkPrefixLimits))
 		if isIP6 {
-			cl.ip6connsPerLimit = countsPerLimit
+			cl.connsPerNetworkPrefixV6 = connsPerNetworkPrefix
 		} else {
-			cl.ip4connsPerLimit = countsPerLimit
+			cl.connsPerNetworkPrefixV4 = connsPerNetworkPrefix
+		}
+	}
+
+	for i, limit := range networkPrefixLimits {
+		if limit.Network.Contains(ip) {
+			if connsPerNetworkPrefix[i]+1 > limit.ConnCount {
+				return false
+			}
+			connsPerNetworkPrefix[i]++
+			// Done. If we find a match in the network prefix limits, we use
+			// that and don't use the general subnet limits.
+			return true
+		}
+	}
+
+	if len(connsPerLimit) == 0 && len(limits) > 0 {
+		connsPerLimit = make([]map[string]int, len(limits))
+		if isIP6 {
+			cl.ip6connsPerLimit = connsPerLimit
+		} else {
+			cl.ip4connsPerLimit = connsPerLimit
 		}
 	}
 
 	for i, limit := range limits {
-		prefix, err := ip.Prefix(limit.BitMask)
+		prefix, err := ip.Prefix(limit.PrefixLength)
 		if err != nil {
 			return false
 		}
 		masked := prefix.String()
-		counts, ok := countsPerLimit[i][masked]
+		counts, ok := connsPerLimit[i][masked]
 		if !ok {
-			if countsPerLimit[i] == nil {
-				countsPerLimit[i] = make(map[string]int)
+			if connsPerLimit[i] == nil {
+				connsPerLimit[i] = make(map[string]int)
 			}
-			countsPerLimit[i][masked] = 0
+			connsPerLimit[i][masked] = 0
 		}
 		if counts+1 > limit.ConnCount {
 			return false
@@ -99,9 +208,9 @@ func (cl *connLimiter) addConn(ip netip.Addr) bool {
 
 	// All limit checks passed, now we update the counts
 	for i, limit := range limits {
-		prefix, _ := ip.Prefix(limit.BitMask)
+		prefix, _ := ip.Prefix(limit.PrefixLength)
 		masked := prefix.String()
-		countsPerLimit[i][masked]++
+		connsPerLimit[i][masked]++
 	}
 
 	return true
@@ -110,31 +219,70 @@ func (cl *connLimiter) addConn(ip netip.Addr) bool {
 func (cl *connLimiter) rmConn(ip netip.Addr) {
 	cl.mu.Lock()
 	defer cl.mu.Unlock()
-	limits := cl.connLimitPerCIDRIP4
-	countsPerLimit := cl.ip4connsPerLimit
+	networkPrefixLimits := cl.networkPrefixLimitV4
+	connsPerNetworkPrefix := cl.connsPerNetworkPrefixV4
+	limits := cl.connLimitPerSubnetV4
+	connsPerLimit := cl.ip4connsPerLimit
 	isIP6 := ip.Is6()
 	if isIP6 {
-		limits = cl.connLimitPerCIDRIP6
-		countsPerLimit = cl.ip6connsPerLimit
+		networkPrefixLimits = cl.networkPrefixLimitV6
+		connsPerNetworkPrefix = cl.connsPerNetworkPrefixV6
+		limits = cl.connLimitPerSubnetV6
+		connsPerLimit = cl.ip6connsPerLimit
+	}
+
+	// Check NetworkPrefix limits first
+	if len(connsPerNetworkPrefix) == 0 && len(networkPrefixLimits) > 0 {
+		// Initialize just in case. We should have already initialized in
+		// addConn, but if the callers calls rmConn first we don't want to panic
+		connsPerNetworkPrefix = make([]int, len(networkPrefixLimits))
+		if isIP6 {
+			cl.connsPerNetworkPrefixV6 = connsPerNetworkPrefix
+		} else {
+			cl.connsPerNetworkPrefixV4 = connsPerNetworkPrefix
+		}
+	}
+	for i, limit := range networkPrefixLimits {
+		if limit.Network.Contains(ip) {
+			count := connsPerNetworkPrefix[i]
+			if count <= 0 {
+				log.Errorf("unexpected conn count for ip %s. Was this not added with addConn first?", ip)
+				return
+			}
+			connsPerNetworkPrefix[i]--
+			// Done. We updated the count in the defined network prefix limit.
+			return
+		}
+	}
+
+	if len(connsPerLimit) == 0 && len(limits) > 0 {
+		// Initialize just in case. We should have already initialized in
+		// addConn, but if the callers calls rmConn first we don't want to panic
+		connsPerLimit = make([]map[string]int, len(limits))
+		if isIP6 {
+			cl.ip6connsPerLimit = connsPerLimit
+		} else {
+			cl.ip4connsPerLimit = connsPerLimit
+		}
 	}
 
 	for i, limit := range limits {
-		prefix, err := ip.Prefix(limit.BitMask)
+		prefix, err := ip.Prefix(limit.PrefixLength)
 		if err != nil {
 			// Unexpected since we should have seen this IP before in addConn
 			log.Errorf("unexpected error getting prefix: %v", err)
 			continue
 		}
 		masked := prefix.String()
-		counts, ok := countsPerLimit[i][masked]
+		counts, ok := connsPerLimit[i][masked]
 		if !ok || counts == 0 {
 			// Unexpected, but don't panic
 			log.Errorf("unexpected conn count for %s ok=%v count=%v", masked, ok, counts)
 			continue
 		}
-		countsPerLimit[i][masked]--
-		if countsPerLimit[i][masked] == 0 {
-			delete(countsPerLimit[i], masked)
+		connsPerLimit[i][masked]--
+		if connsPerLimit[i][masked] <= 0 {
+			delete(connsPerLimit[i], masked)
 		}
 	}
 }
diff --git a/p2p/host/resource-manager/rcmgr.go b/p2p/host/resource-manager/rcmgr.go
index 739657c2..4b9fda01 100644
--- a/p2p/host/resource-manager/rcmgr.go
+++ b/p2p/host/resource-manager/rcmgr.go
@@ -148,6 +148,28 @@ func NewResourceManager(limits Limiter, opts ...Option) (network.ResourceManager
 		}
 	}
 
+	registeredConnLimiterPrefixes := make(map[string]struct{})
+	for _, npLimit := range r.connLimiter.networkPrefixLimitV4 {
+		registeredConnLimiterPrefixes[npLimit.Network.String()] = struct{}{}
+	}
+	for _, npLimit := range r.connLimiter.networkPrefixLimitV6 {
+		registeredConnLimiterPrefixes[npLimit.Network.String()] = struct{}{}
+	}
+	for _, network := range allowlist.allowedNetworks {
+		prefix, err := netip.ParsePrefix(network.String())
+		if err != nil {
+			log.Debugf("failed to parse prefix from allowlist %s, %s", network, err)
+			continue
+		}
+		if _, ok := registeredConnLimiterPrefixes[prefix.String()]; !ok {
+			// connlimiter doesn't know about this network. Let's fix that
+			r.connLimiter.addNetworkPrefixLimit(prefix.Addr().Is6(), NetworkPrefixLimit{
+				Network:   prefix,
+				ConnCount: r.limits.GetAllowlistedSystemLimits().GetConnTotalLimit(),
+			})
+		}
+	}
+
 	if !r.disableMetrics {
 		var sr TraceReporter
 		sr, err := NewStatsTraceReporter()


Test patch:
diff --git a/p2p/host/resource-manager/conn_limiter_test.go b/p2p/host/resource-manager/conn_limiter_test.go
index 9c871c4c..89ce5d53 100644
--- a/p2p/host/resource-manager/conn_limiter_test.go
+++ b/p2p/host/resource-manager/conn_limiter_test.go
@@ -2,7 +2,6 @@ package rcmgr
 
 import (
 	"encoding/binary"
-	"fmt"
 	"net"
 	"net/netip"
 	"testing"
@@ -15,7 +14,7 @@ func TestItLimits(t *testing.T) {
 		ip, err := netip.ParseAddr("1.2.3.4")
 		require.NoError(t, err)
 		cl := newConnLimiter()
-		cl.connLimitPerCIDRIP4[0].ConnCount = 1
+		cl.connLimitPerSubnetV4[0].ConnCount = 1
 		require.True(t, cl.addConn(ip))
 
 		// should fail the second time
@@ -29,10 +28,10 @@ func TestItLimits(t *testing.T) {
 		ip, err := netip.ParseAddr("1:2:3:4::1")
 		require.NoError(t, err)
 		cl := newConnLimiter()
-		original := cl.connLimitPerCIDRIP6[0].ConnCount
-		cl.connLimitPerCIDRIP6[0].ConnCount = 1
+		original := cl.connLimitPerSubnetV6[0].ConnCount
+		cl.connLimitPerSubnetV6[0].ConnCount = 1
 		defer func() {
-			cl.connLimitPerCIDRIP6[0].ConnCount = original
+			cl.connLimitPerSubnetV6[0].ConnCount = original
 		}()
 		require.True(t, cl.addConn(ip))
 
@@ -67,7 +66,6 @@ func TestItLimits(t *testing.T) {
 		for i := 0; i < defaultMaxConcurrentConns*8; i++ {
 			ip := net.ParseIP("ffef:2:3:4::1")
 			binary.BigEndian.PutUint16(ip[5:7], uint16(i))
-			fmt.Println(ip.String())
 			ipAddr := netip.MustParseAddr(ip.String())
 			require.True(t, cl.addConn(ipAddr))
 		}
@@ -77,6 +75,26 @@ func TestItLimits(t *testing.T) {
 		ipAddr := netip.MustParseAddr(ip.String())
 		require.False(t, cl.addConn(ipAddr))
 	})
+
+	t.Run("IPv4 with localhost", func(t *testing.T) {
+		cl := &connLimiter{
+			networkPrefixLimitV4: DefaultNetworkPrefixLimitV4,
+			connLimitPerSubnetV4: []ConnLimitPerSubnet{
+				{PrefixLength: 0, ConnCount: 1}, // 1 connection for the whole IPv4 space
+			},
+		}
+
+		ip := netip.MustParseAddr("1.2.3.4")
+		require.True(t, cl.addConn(ip))
+
+		ip = netip.MustParseAddr("4.3.2.1")
+		// should fail the second time, we only allow 1 connection for the whole IPv4 space
+		require.False(t, cl.addConn(ip))
+
+		ip = netip.MustParseAddr("127.0.0.1")
+		// Succeeds because we defined an explicit limit for the loopback subnet
+		require.True(t, cl.addConn(ip))
+	})
 }
 
 func genIP(data *[]byte) (netip.Addr, bool) {
@@ -131,6 +149,12 @@ func FuzzConnLimiter(f *testing.F) {
 				addedCount += count
 			}
 		}
+		for _, count := range cl.connsPerNetworkPrefixV4 {
+			addedCount += count
+		}
+		for _, count := range cl.connsPerNetworkPrefixV6 {
+			addedCount += count
+		}
 		if addedCount == 0 && len(addedConns) > 0 {
 			t.Fatalf("added count: %d", addedCount)
 		}
@@ -150,9 +174,41 @@ func FuzzConnLimiter(f *testing.F) {
 				leftoverCount += count
 			}
 		}
+		for _, count := range cl.connsPerNetworkPrefixV4 {
+			addedCount += count
+		}
+		for _, count := range cl.connsPerNetworkPrefixV6 {
+			addedCount += count
+		}
 		if leftoverCount != 0 {
 			t.Fatalf("leftover count: %d", leftoverCount)
 		}
 	})
+}
 
+func TestSortedNetworkPrefixLimits(t *testing.T) {
+	npLimits := []NetworkPrefixLimit{
+		{
+			Network: netip.MustParsePrefix("1.2.0.0/16"),
+		},
+		{
+			Network: netip.MustParsePrefix("1.2.3.0/28"),
+		},
+		{
+			Network: netip.MustParsePrefix("1.2.3.4/32"),
+		},
+	}
+	npLimits = sortNetworkPrefixes(npLimits)
+	sorted := []NetworkPrefixLimit{
+		{
+			Network: netip.MustParsePrefix("1.2.3.4/32"),
+		},
+		{
+			Network: netip.MustParsePrefix("1.2.3.0/28"),
+		},
+		{
+			Network: netip.MustParsePrefix("1.2.0.0/16"),
+		},
+	}
+	require.EqualValues(t, sorted, npLimits)
 }
diff --git a/p2p/host/resource-manager/rcmgr_test.go b/p2p/host/resource-manager/rcmgr_test.go
index 61d53beb..bf79f7aa 100644
--- a/p2p/host/resource-manager/rcmgr_test.go
+++ b/p2p/host/resource-manager/rcmgr_test.go
@@ -1,12 +1,14 @@
 package rcmgr
 
 import (
+	"net/netip"
 	"testing"
 
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/test"
+	"github.com/stretchr/testify/require"
 
 	"github.com/multiformats/go-multiaddr"
 )
@@ -1051,3 +1053,61 @@ func TestResourceManagerWithAllowlist(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+// TestAllowlistAndConnLimiterPlayNice checks that the connLimiter learns about network prefix limits from the allowlist.
+func TestAllowlistAndConnLimiterPlayNice(t *testing.T) {
+	limits := DefaultLimits.AutoScale()
+	limits.allowlistedSystem.Conns = 8
+	limits.allowlistedSystem.ConnsInbound = 8
+	limits.allowlistedSystem.ConnsOutbound = 8
+	t.Run("IPv4", func(t *testing.T) {
+		rcmgr, err := NewResourceManager(NewFixedLimiter(limits), WithAllowlistedMultiaddrs([]multiaddr.Multiaddr{
+			multiaddr.StringCast("/ip4/1.2.3.0/ipcidr/24"),
+		}), WithNetworkPrefixLimit([]NetworkPrefixLimit{}, []NetworkPrefixLimit{}))
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer rcmgr.Close()
+
+		// The connLimiter should have the allowlisted network prefix
+		require.Equal(t, netip.MustParsePrefix("1.2.3.0/24"), rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].Network)
+
+		// The connLimiter should use the limit from the allowlist
+		require.Equal(t, 8, rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].ConnCount)
+	})
+	t.Run("IPv6", func(t *testing.T) {
+		rcmgr, err := NewResourceManager(NewFixedLimiter(limits), WithAllowlistedMultiaddrs([]multiaddr.Multiaddr{
+			multiaddr.StringCast("/ip6/1:2:3::/ipcidr/58"),
+		}), WithNetworkPrefixLimit([]NetworkPrefixLimit{}, []NetworkPrefixLimit{}))
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer rcmgr.Close()
+
+		// The connLimiter should have the allowlisted network prefix
+		require.Equal(t, netip.MustParsePrefix("1:2:3::/58"), rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV6[0].Network)
+
+		// The connLimiter should use the limit from the allowlist
+		require.Equal(t, 8, rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV6[0].ConnCount)
+	})
+
+	t.Run("Does not override if you set a limit directly", func(t *testing.T) {
+		rcmgr, err := NewResourceManager(NewFixedLimiter(limits), WithAllowlistedMultiaddrs([]multiaddr.Multiaddr{
+			multiaddr.StringCast("/ip4/1.2.3.0/ipcidr/24"),
+		}), WithNetworkPrefixLimit([]NetworkPrefixLimit{
+			{Network: netip.MustParsePrefix("1.2.3.0/24"), ConnCount: 1},
+		}, []NetworkPrefixLimit{}))
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer rcmgr.Close()
+
+		// The connLimiter should have it because we set it
+		require.Equal(t, netip.MustParsePrefix("1.2.3.0/24"), rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].Network)
+		// should only have one network prefix limit
+		require.Equal(t, 1, len(rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4))
+
+		// The connLimiter should use the limit we defined explicitly
+		require.Equal(t, 1, rcmgr.(*resourceManager).connLimiter.networkPrefixLimitV4[0].ConnCount)
+	})
+}