Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use CIDR format for connection-manager allow/deny lists #2783

Merged
merged 9 commits into from
Dec 9, 2024
1 change: 1 addition & 0 deletions packages/libp2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
},
"dependencies": {
"@chainsafe/is-ip": "^2.0.2",
"@chainsafe/netmask": "^2.0.0",
"@libp2p/crypto": "^5.0.7",
"@libp2p/interface": "^2.2.1",
"@libp2p/interface-internal": "^2.1.1",
Expand Down
10 changes: 6 additions & 4 deletions packages/libp2p/src/connection-manager/connection-pruner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PeerMap } from '@libp2p/peer-collections'
import { safelyCloseConnectionIfUnused } from '@libp2p/utils/close'
import { MAX_CONNECTIONS } from './constants.js'
import { multiaddrToIpNet } from './utils.js'
import type { IpNet } from '@chainsafe/netmask'
import type { Libp2pEvents, Logger, ComponentLogger, TypedEventTarget, PeerStore, Connection } from '@libp2p/interface'
import type { ConnectionManager } from '@libp2p/interface-internal'
import type { Multiaddr } from '@multiformats/multiaddr'
Expand Down Expand Up @@ -29,13 +31,13 @@ export class ConnectionPruner {
private readonly maxConnections: number
private readonly connectionManager: ConnectionManager
private readonly peerStore: PeerStore
private readonly allow: Multiaddr[]
private readonly allow: IpNet[]
private readonly events: TypedEventTarget<Libp2pEvents>
private readonly log: Logger

constructor (components: ConnectionPrunerComponents, init: ConnectionPrunerInit = {}) {
this.maxConnections = init.maxConnections ?? defaultOptions.maxConnections
this.allow = init.allow ?? defaultOptions.allow
this.allow = (init.allow ?? []).map(ma => multiaddrToIpNet(ma))
this.connectionManager = components.connectionManager
this.peerStore = components.peerStore
this.events = components.events
Expand Down Expand Up @@ -107,8 +109,8 @@ export class ConnectionPruner {
for (const connection of sortedConnections) {
this.log('too many connections open - closing a connection to %p', connection.remotePeer)
// check allow list
const connectionInAllowList = this.allow.some((ma) => {
return connection.remoteAddr.toString().startsWith(ma.toString())
const connectionInAllowList = this.allow.some((ipNet) => {
return ipNet.contains(connection.remoteAddr.nodeAddress().address)
})

// Connections in the allow list should be excluded from pruning
Expand Down
18 changes: 10 additions & 8 deletions packages/libp2p/src/connection-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ConnectionPruner } from './connection-pruner.js'
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'
import { DialQueue } from './dial-queue.js'
import { ReconnectQueue } from './reconnect-queue.js'
import { multiaddrToIpNet } from './utils.js'
import type { IpNet } from '@chainsafe/netmask'
import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, PeerStore, Startable, PendingDialStatus, PeerRouting, IsDialableOptions } from '@libp2p/interface'
import type { ConnectionManager, OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal'
import type { JobStatus } from '@libp2p/utils/queue'
Expand Down Expand Up @@ -176,8 +178,8 @@ export interface DefaultConnectionManagerComponents {
export class DefaultConnectionManager implements ConnectionManager, Startable {
private started: boolean
private readonly connections: PeerMap<Connection[]>
private readonly allow: Multiaddr[]
private readonly deny: Multiaddr[]
private readonly allow: IpNet[]
private readonly deny: IpNet[]
private readonly maxIncomingPendingConnections: number
private incomingPendingConnections: number
private outboundPendingConnections: number
Expand Down Expand Up @@ -216,8 +218,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
this.onDisconnect = this.onDisconnect.bind(this)

// allow/deny lists
this.allow = (init.allow ?? []).map(ma => multiaddr(ma))
this.deny = (init.deny ?? []).map(ma => multiaddr(ma))
this.allow = (init.allow ?? []).map(str => multiaddrToIpNet(str))
this.deny = (init.deny ?? []).map(str => multiaddrToIpNet(str))

this.incomingPendingConnections = 0
this.maxIncomingPendingConnections = init.maxIncomingPendingConnections ?? defaultOptions.maxIncomingPendingConnections
Expand All @@ -237,7 +239,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
logger: components.logger
}, {
maxConnections: this.maxConnections,
allow: this.allow
allow: init.allow?.map(a => multiaddr(a))
})

this.dialQueue = new DialQueue(components, {
Expand Down Expand Up @@ -575,7 +577,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
async acceptIncomingConnection (maConn: MultiaddrConnection): Promise<boolean> {
acul71 marked this conversation as resolved.
Show resolved Hide resolved
// check deny list
const denyConnection = this.deny.some(ma => {
return maConn.remoteAddr.toString().startsWith(ma.toString())
return ma.contains(maConn.remoteAddr.nodeAddress().address)
})
acul71 marked this conversation as resolved.
Show resolved Hide resolved

if (denyConnection) {
Expand All @@ -584,8 +586,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
}

// check allow list
const allowConnection = this.allow.some(ma => {
return maConn.remoteAddr.toString().startsWith(ma.toString())
const allowConnection = this.allow.some(ipNet => {
return ipNet.contains(maConn.remoteAddr.nodeAddress().address)
})

if (allowConnection) {
Expand Down
37 changes: 35 additions & 2 deletions packages/libp2p/src/connection-manager/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { resolvers } from '@multiformats/multiaddr'
import { multiaddr, resolvers, type Multiaddr, type ResolveOptions } from '@multiformats/multiaddr'
import { convertToIpNet } from '@multiformats/multiaddr/convert'
import type { IpNet } from '@chainsafe/netmask'
import type { LoggerOptions } from '@libp2p/interface'
import type { Multiaddr, ResolveOptions } from '@multiformats/multiaddr'

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

return output
}

/**
* Converts a multiaddr string or object to an IpNet object.
* If the multiaddr doesn't include /ipcidr, it will encapsulate with the appropriate CIDR:
* - /ipcidr/32 for IPv4
* - /ipcidr/128 for IPv6
*
* @param {string | Multiaddr} ma - The multiaddr string or object to convert.
* @returns {IpNet} The converted IpNet object.
* @throws {Error} Throws an error if the multiaddr is not valid.
*/
export function multiaddrToIpNet (ma: string | Multiaddr): IpNet {
try {
let parsedMa: Multiaddr
if (typeof ma === 'string') {
parsedMa = multiaddr(ma)
} else {
parsedMa = ma
}

// Check if /ipcidr is already present
if (!parsedMa.protoNames().includes('ipcidr')) {
const isIPv6 = parsedMa.protoNames().includes('ip6')
const cidr = isIPv6 ? '/ipcidr/128' : '/ipcidr/32'
acul71 marked this conversation as resolved.
Show resolved Hide resolved
parsedMa = parsedMa.encapsulate(cidr)
}

return convertToIpNet(parsedMa)
} catch (error) {
throw new Error(`Can't convert to IpNet, Invalid multiaddr format: ${ma}`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,44 @@ describe('connection-pruner', () => {
expect(shortestLivedWithLowestTagSpy).to.have.property('callCount', 1)
})

it('should correctly parse and store allow list as IpNet objects in ConnectionPruner', () => {
const mockInit = {
allow: [
multiaddr('/ip4/83.13.55.32/ipcidr/32'),
multiaddr('/ip4/83.13.55.32'),
multiaddr('/ip4/192.168.1.1/ipcidr/24'),
multiaddr('/ip6/2001::0/ipcidr/64')
]
}

// Create a ConnectionPruner instance
const pruner = new ConnectionPruner(components, mockInit)

// Expected IpNet objects for comparison
const expectedAllowList = [
{
mask: new Uint8Array([255, 255, 255, 255]),
network: new Uint8Array([83, 13, 55, 32])
},
{
mask: new Uint8Array([255, 255, 255, 255]),
network: new Uint8Array([83, 13, 55, 32])
},
{
mask: new Uint8Array([255, 255, 255, 0]),
network: new Uint8Array([192, 168, 1, 0])
},
{
mask: new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0]),
network: new Uint8Array([32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
}
]

// Verify that the allow list in the pruner matches the expected IpNet objects
// eslint-disable-next-line @typescript-eslint/dot-notation
expect(pruner['allow']).to.deep.equal(expectedAllowList)
})

it('should not close connection that is on the allowlist when pruning', async () => {
const max = 2
const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283')
Expand All @@ -241,6 +279,7 @@ describe('connection-pruner', () => {
for (let i = 0; i < max; i++) {
const connection = stubInterface<Connection>({
remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')),
remoteAddr: multiaddr('/ip4/127.0.0.1/tcp/12345'),
streams: []
})
const spy = connection.close
Expand Down Expand Up @@ -269,7 +308,6 @@ describe('connection-pruner', () => {
const value = 0
const spy = connection.close
spies.set(value, spy)

// Tag that allowed peer with lowest value
components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface<Peer>({
tags: new Map([['test-tag', { value }]])
Expand Down
Loading
Loading