Skip to content

Commit 48e9cfa

Browse files
acul71achingbrain
andauthored
feat: Use CIDR format for connection-manager allow/deny lists (#2783)
Updates the connection manager to treat multiaddrs in the allow/deny lists using the standard IP CIDR format (e.g. `/ip4/52.55.0.0/ipcidr/16`) rather than string prefixes (e.g. `/ip4/52.55`). This allows us to validate multiaddrs accurately and ensures better control over IP address matching. --------- Co-authored-by: Alex Potsides <[email protected]>
1 parent 9665411 commit 48e9cfa

File tree

7 files changed

+318
-17
lines changed

7 files changed

+318
-17
lines changed

packages/libp2p/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
},
8787
"dependencies": {
8888
"@chainsafe/is-ip": "^2.0.2",
89+
"@chainsafe/netmask": "^2.0.0",
8990
"@libp2p/crypto": "^5.0.7",
9091
"@libp2p/interface": "^2.2.1",
9192
"@libp2p/interface-internal": "^2.1.1",

packages/libp2p/src/connection-manager/connection-pruner.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { PeerMap } from '@libp2p/peer-collections'
22
import { safelyCloseConnectionIfUnused } from '@libp2p/utils/close'
33
import { MAX_CONNECTIONS } from './constants.js'
4+
import { multiaddrToIpNet } from './utils.js'
5+
import type { IpNet } from '@chainsafe/netmask'
46
import type { Libp2pEvents, Logger, ComponentLogger, TypedEventTarget, PeerStore, Connection } from '@libp2p/interface'
57
import type { ConnectionManager } from '@libp2p/interface-internal'
68
import type { Multiaddr } from '@multiformats/multiaddr'
@@ -29,13 +31,13 @@ export class ConnectionPruner {
2931
private readonly maxConnections: number
3032
private readonly connectionManager: ConnectionManager
3133
private readonly peerStore: PeerStore
32-
private readonly allow: Multiaddr[]
34+
private readonly allow: IpNet[]
3335
private readonly events: TypedEventTarget<Libp2pEvents>
3436
private readonly log: Logger
3537

3638
constructor (components: ConnectionPrunerComponents, init: ConnectionPrunerInit = {}) {
3739
this.maxConnections = init.maxConnections ?? defaultOptions.maxConnections
38-
this.allow = init.allow ?? defaultOptions.allow
40+
this.allow = (init.allow ?? []).map(ma => multiaddrToIpNet(ma))
3941
this.connectionManager = components.connectionManager
4042
this.peerStore = components.peerStore
4143
this.events = components.events
@@ -107,8 +109,8 @@ export class ConnectionPruner {
107109
for (const connection of sortedConnections) {
108110
this.log('too many connections open - closing a connection to %p', connection.remotePeer)
109111
// check allow list
110-
const connectionInAllowList = this.allow.some((ma) => {
111-
return connection.remoteAddr.toString().startsWith(ma.toString())
112+
const connectionInAllowList = this.allow.some((ipNet) => {
113+
return ipNet.contains(connection.remoteAddr.nodeAddress().address)
112114
})
113115

114116
// Connections in the allow list should be excluded from pruning

packages/libp2p/src/connection-manager/index.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { ConnectionPruner } from './connection-pruner.js'
99
import { DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_DIAL_QUEUE_LENGTH, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL } from './constants.js'
1010
import { DialQueue } from './dial-queue.js'
1111
import { ReconnectQueue } from './reconnect-queue.js'
12+
import { multiaddrToIpNet } from './utils.js'
13+
import type { IpNet } from '@chainsafe/netmask'
1214
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, PeerStore, Startable, PendingDialStatus, PeerRouting, IsDialableOptions } from '@libp2p/interface'
1315
import type { ConnectionManager, OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal'
1416
import type { JobStatus } from '@libp2p/utils/queue'
@@ -176,8 +178,8 @@ export interface DefaultConnectionManagerComponents {
176178
export class DefaultConnectionManager implements ConnectionManager, Startable {
177179
private started: boolean
178180
private readonly connections: PeerMap<Connection[]>
179-
private readonly allow: Multiaddr[]
180-
private readonly deny: Multiaddr[]
181+
private readonly allow: IpNet[]
182+
private readonly deny: IpNet[]
181183
private readonly maxIncomingPendingConnections: number
182184
private incomingPendingConnections: number
183185
private outboundPendingConnections: number
@@ -216,8 +218,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
216218
this.onDisconnect = this.onDisconnect.bind(this)
217219

218220
// allow/deny lists
219-
this.allow = (init.allow ?? []).map(ma => multiaddr(ma))
220-
this.deny = (init.deny ?? []).map(ma => multiaddr(ma))
221+
this.allow = (init.allow ?? []).map(str => multiaddrToIpNet(str))
222+
this.deny = (init.deny ?? []).map(str => multiaddrToIpNet(str))
221223

222224
this.incomingPendingConnections = 0
223225
this.maxIncomingPendingConnections = init.maxIncomingPendingConnections ?? defaultOptions.maxIncomingPendingConnections
@@ -237,7 +239,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
237239
logger: components.logger
238240
}, {
239241
maxConnections: this.maxConnections,
240-
allow: this.allow
242+
allow: init.allow?.map(a => multiaddr(a))
241243
})
242244

243245
this.dialQueue = new DialQueue(components, {
@@ -575,7 +577,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
575577
async acceptIncomingConnection (maConn: MultiaddrConnection): Promise<boolean> {
576578
// check deny list
577579
const denyConnection = this.deny.some(ma => {
578-
return maConn.remoteAddr.toString().startsWith(ma.toString())
580+
return ma.contains(maConn.remoteAddr.nodeAddress().address)
579581
})
580582

581583
if (denyConnection) {
@@ -584,8 +586,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
584586
}
585587

586588
// check allow list
587-
const allowConnection = this.allow.some(ma => {
588-
return maConn.remoteAddr.toString().startsWith(ma.toString())
589+
const allowConnection = this.allow.some(ipNet => {
590+
return ipNet.contains(maConn.remoteAddr.nodeAddress().address)
589591
})
590592

591593
if (allowConnection) {

packages/libp2p/src/connection-manager/utils.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { resolvers } from '@multiformats/multiaddr'
1+
import { multiaddr, resolvers, type Multiaddr, type ResolveOptions } from '@multiformats/multiaddr'
2+
import { convertToIpNet } from '@multiformats/multiaddr/convert'
3+
import type { IpNet } from '@chainsafe/netmask'
24
import type { LoggerOptions } from '@libp2p/interface'
3-
import type { Multiaddr, ResolveOptions } from '@multiformats/multiaddr'
45

56
/**
67
* Recursively resolve DNSADDR multiaddrs
@@ -28,3 +29,35 @@ export async function resolveMultiaddrs (ma: Multiaddr, options: ResolveOptions
2829

2930
return output
3031
}
32+
33+
/**
34+
* Converts a multiaddr string or object to an IpNet object.
35+
* If the multiaddr doesn't include /ipcidr, it will encapsulate with the appropriate CIDR:
36+
* - /ipcidr/32 for IPv4
37+
* - /ipcidr/128 for IPv6
38+
*
39+
* @param {string | Multiaddr} ma - The multiaddr string or object to convert.
40+
* @returns {IpNet} The converted IpNet object.
41+
* @throws {Error} Throws an error if the multiaddr is not valid.
42+
*/
43+
export function multiaddrToIpNet (ma: string | Multiaddr): IpNet {
44+
try {
45+
let parsedMa: Multiaddr
46+
if (typeof ma === 'string') {
47+
parsedMa = multiaddr(ma)
48+
} else {
49+
parsedMa = ma
50+
}
51+
52+
// Check if /ipcidr is already present
53+
if (!parsedMa.protoNames().includes('ipcidr')) {
54+
const isIPv6 = parsedMa.protoNames().includes('ip6')
55+
const cidr = isIPv6 ? '/ipcidr/128' : '/ipcidr/32'
56+
parsedMa = parsedMa.encapsulate(cidr)
57+
}
58+
59+
return convertToIpNet(parsedMa)
60+
} catch (error) {
61+
throw new Error(`Can't convert to IpNet, Invalid multiaddr format: ${ma}`)
62+
}
63+
}

packages/libp2p/test/connection-manager/connection-pruner.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,44 @@ describe('connection-pruner', () => {
219219
expect(shortestLivedWithLowestTagSpy).to.have.property('callCount', 1)
220220
})
221221

222+
it('should correctly parse and store allow list as IpNet objects in ConnectionPruner', () => {
223+
const mockInit = {
224+
allow: [
225+
multiaddr('/ip4/83.13.55.32/ipcidr/32'),
226+
multiaddr('/ip4/83.13.55.32'),
227+
multiaddr('/ip4/192.168.1.1/ipcidr/24'),
228+
multiaddr('/ip6/2001::0/ipcidr/64')
229+
]
230+
}
231+
232+
// Create a ConnectionPruner instance
233+
const pruner = new ConnectionPruner(components, mockInit)
234+
235+
// Expected IpNet objects for comparison
236+
const expectedAllowList = [
237+
{
238+
mask: new Uint8Array([255, 255, 255, 255]),
239+
network: new Uint8Array([83, 13, 55, 32])
240+
},
241+
{
242+
mask: new Uint8Array([255, 255, 255, 255]),
243+
network: new Uint8Array([83, 13, 55, 32])
244+
},
245+
{
246+
mask: new Uint8Array([255, 255, 255, 0]),
247+
network: new Uint8Array([192, 168, 1, 0])
248+
},
249+
{
250+
mask: new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0]),
251+
network: new Uint8Array([32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
252+
}
253+
]
254+
255+
// Verify that the allow list in the pruner matches the expected IpNet objects
256+
// eslint-disable-next-line @typescript-eslint/dot-notation
257+
expect(pruner['allow']).to.deep.equal(expectedAllowList)
258+
})
259+
222260
it('should not close connection that is on the allowlist when pruning', async () => {
223261
const max = 2
224262
const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283')
@@ -241,6 +279,7 @@ describe('connection-pruner', () => {
241279
for (let i = 0; i < max; i++) {
242280
const connection = stubInterface<Connection>({
243281
remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')),
282+
remoteAddr: multiaddr('/ip4/127.0.0.1/tcp/12345'),
244283
streams: []
245284
})
246285
const spy = connection.close
@@ -269,7 +308,6 @@ describe('connection-pruner', () => {
269308
const value = 0
270309
const spy = connection.close
271310
spies.set(value, spy)
272-
273311
// Tag that allowed peer with lowest value
274312
components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface<Peer>({
275313
tags: new Map([['test-tag', { value }]])

0 commit comments

Comments
 (0)