Skip to content

Commit 84c3037

Browse files
acul71achingbrain
authored andcommitted
feat: Use CIDR format for connection-manager allow/deny lists
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.
1 parent 06f79b6 commit 84c3037

File tree

7 files changed

+349
-17
lines changed

7 files changed

+349
-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: 15 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'
3+
import { convertToIpNet } from '@multiformats/multiaddr/convert'
34
import { MAX_CONNECTIONS } from './constants.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,22 @@ 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) => {
41+
try {
42+
if (!ma.protoNames().includes('ipcidr')) {
43+
ma = ma.encapsulate('/ipcidr/32') // Encapsulate with /ipcidr/32 if missing
44+
}
45+
return convertToIpNet(ma)
46+
} catch (error) {
47+
throw new Error(`Invalid multiaddr format in allow list: ${ma}`)
48+
}
49+
})
3950
this.connectionManager = components.connectionManager
4051
this.peerStore = components.peerStore
4152
this.events = components.events
@@ -107,8 +118,8 @@ export class ConnectionPruner {
107118
for (const connection of sortedConnections) {
108119
this.log('too many connections open - closing a connection to %p', connection.remotePeer)
109120
// check allow list
110-
const connectionInAllowList = this.allow.some((ma) => {
111-
return connection.remoteAddr.toString().startsWith(ma.toString())
121+
const connectionInAllowList = this.allow.some((ipNet) => {
122+
return ipNet.contains(connection.remoteAddr.nodeAddress().address)
112123
})
113124

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

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

Lines changed: 34 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 != null ? this.parseIpNetList(init.allow) : []
222+
this.deny = init.deny != null ? this.parseIpNetList(init.deny) : []
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 != null ? init.allow.map(a => multiaddr(a)) : undefined
241243
})
242244

243245
this.dialQueue = new DialQueue(components, {
@@ -265,6 +267,30 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
265267
})
266268
}
267269

270+
/**
271+
* Parses a list of IP addresses or CIDR notation strings into an array of IpNet objects.
272+
*
273+
* @param {string[]} list - The list of IP addresses or CIDR strings to parse.
274+
* @returns {IpNet[]} An array of IpNet objects derived from the provided list.
275+
* @throws {Error} Throws an error if any string in the list is not a valid multiaddr format.
276+
*/
277+
private parseIpNetList (list: string[]): IpNet[] {
278+
return list.map((a) => {
279+
try {
280+
// Attempt to parse `a` with the required /ipcidr/32 if missing
281+
let ma
282+
if (a.includes('/ipcidr')) {
283+
ma = multiaddr(a) // Parse directly if it already includes /ipcidr
284+
} else {
285+
ma = multiaddr(a).encapsulate('/ipcidr/32') // Encapsulate with /ipcidr/32 if missing
286+
}
287+
return multiaddrToIpNet(ma)
288+
} catch (error) {
289+
throw new Error(`Invalid multiaddr format in list: ${a}`)
290+
}
291+
})
292+
}
293+
268294
readonly [Symbol.toStringTag] = '@libp2p/connection-manager'
269295

270296
/**
@@ -571,7 +597,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
571597
async acceptIncomingConnection (maConn: MultiaddrConnection): Promise<boolean> {
572598
// check deny list
573599
const denyConnection = this.deny.some(ma => {
574-
return maConn.remoteAddr.toString().startsWith(ma.toString())
600+
return ma.contains(maConn.remoteAddr.nodeAddress().address)
575601
})
576602

577603
if (denyConnection) {
@@ -580,8 +606,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
580606
}
581607

582608
// check allow list
583-
const allowConnection = this.allow.some(ma => {
584-
return maConn.remoteAddr.toString().startsWith(ma.toString())
609+
const allowConnection = this.allow.some(ipNet => {
610+
return ipNet.contains(maConn.remoteAddr.nodeAddress().address)
585611
})
586612

587613
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: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,39 @@ 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+
]
229+
}
230+
231+
// Create a ConnectionPruner instance
232+
const pruner = new ConnectionPruner(components, mockInit)
233+
234+
// Expected IpNet objects for comparison
235+
const expectedAllowList = [
236+
{
237+
mask: new Uint8Array([255, 255, 255, 255]),
238+
network: new Uint8Array([83, 13, 55, 32])
239+
},
240+
{
241+
mask: new Uint8Array([255, 255, 255, 255]),
242+
network: new Uint8Array([83, 13, 55, 32])
243+
},
244+
{
245+
mask: new Uint8Array([255, 255, 255, 0]),
246+
network: new Uint8Array([192, 168, 1, 0])
247+
}
248+
]
249+
250+
// Verify that the allow list in the pruner matches the expected IpNet objects
251+
// eslint-disable-next-line @typescript-eslint/dot-notation
252+
expect(pruner['allow']).to.deep.equal(expectedAllowList)
253+
})
254+
222255
it('should not close connection that is on the allowlist when pruning', async () => {
223256
const max = 2
224257
const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283')
@@ -241,6 +274,7 @@ describe('connection-pruner', () => {
241274
for (let i = 0; i < max; i++) {
242275
const connection = stubInterface<Connection>({
243276
remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')),
277+
remoteAddr: multiaddr('/ip4/127.0.0.1/tcp/12345'),
244278
streams: []
245279
})
246280
const spy = connection.close
@@ -269,7 +303,6 @@ describe('connection-pruner', () => {
269303
const value = 0
270304
const spy = connection.close
271305
spies.set(value, spy)
272-
273306
// Tag that allowed peer with lowest value
274307
components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface<Peer>({
275308
tags: new Map([['test-tag', { value }]])

0 commit comments

Comments
 (0)