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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/libp2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"test:webkit": "aegir test -t browser -- --browser webkit"
},
"dependencies": {
"@chainsafe/netmask": "^2.0.0",
"@libp2p/crypto": "^5.0.6",
"@libp2p/interface": "^2.2.0",
"@libp2p/interface-internal": "^2.0.10",
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 { multiaddrToIpNet } from '@libp2p/utils/multiaddrToIpNet'
import { MAX_CONNECTIONS } from './constants.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
28 changes: 20 additions & 8 deletions packages/libp2p/src/connection-manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type IpNet } from '@chainsafe/netmask'
import { ConnectionClosedError, InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'
import { PeerMap } from '@libp2p/peer-collections'
import { multiaddrToIpNet } from '@libp2p/utils/multiaddrToIpNet'
import { RateLimiter } from '@libp2p/utils/rate-limiter'
import { type Multiaddr, type Resolver, multiaddr } from '@multiformats/multiaddr'
import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
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 != null ? this.parseIpNetList(init.allow) : []
this.deny = init.deny != null ? this.parseIpNetList(init.deny) : []

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 != null ? init.allow.map(a => multiaddr(a)) : undefined
})

this.dialQueue = new DialQueue(components, {
Expand Down Expand Up @@ -265,6 +267,16 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
})
}

/**
* Converts a list of string representations of multiaddresses into an array of IpNet objects.
*
* @param list - An array of strings, each representing a multiaddress.
* @returns An array of IpNet objects parsed from the given multiaddress strings.
*/
private parseIpNetList (list: string[]): IpNet[] {
return list.map(a => multiaddrToIpNet(a))
}

readonly [Symbol.toStringTag] = '@libp2p/connection-manager'

/**
Expand Down Expand Up @@ -571,7 +583,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
async acceptIncomingConnection (maConn: MultiaddrConnection): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you add some tests that show that incoming connections are denied for multiaddrs in the deny list and allowed for ones in the allow list.

They should show that individual addresses and ranges are handled correctly in both IPv4 and IPv6 formats.

It should be a case of adding more tests like this one and this one.

// 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 @@ -580,8 +592,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
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,39 @@ 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')
]
}

// 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])
}
]

// 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 +274,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
42 changes: 42 additions & 0 deletions packages/libp2p/test/connection-manager/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,48 @@ describe('Connection Manager', () => {
await stop(connectionManager, libp2p)
})

it('should correctly parse and store allow and deny lists as IpNet objects in ConnectionManager', () => {
// Define common IPs and CIDRs for reuse
const ipAllowDeny = [
'/ip4/83.13.55.32', // Single IP address
'/ip4/83.13.55.32/ipcidr/32', // CIDR notation for a single IP
'/ip4/192.168.1.1/ipcidr/24' // CIDR notation for a network
]

// Initialize mock input for the allow and deny lists
const mockInit = {
allow: [...ipAllowDeny],
deny: [...ipAllowDeny]
}

// Create an instance of the DefaultConnectionManager with the mock initialization
const connectionManager = new DefaultConnectionManager(components, mockInit)

// Define the expected IpNet objects that should result from parsing the allow and deny lists
const expectedIpNets = [
{
mask: new Uint8Array([255, 255, 255, 255]), // Netmask for a single IP address
network: new Uint8Array([83, 13, 55, 32]) // Network address for '83.13.55.32'
},
{
mask: new Uint8Array([255, 255, 255, 255]), // Netmask for a single IP address
network: new Uint8Array([83, 13, 55, 32]) // Network address for '83.13.55.32'
},
{
mask: new Uint8Array([255, 255, 255, 0]), // Netmask for a /24 CIDR block
network: new Uint8Array([192, 168, 1, 0]) // Network address for '192.168.1.0'
}
]

// Test that the 'allow' list is correctly parsed and stored as IpNet objects
// eslint-disable-next-line @typescript-eslint/dot-notation
expect(connectionManager['allow']).to.deep.equal(expectedIpNets)

// Test that the 'deny' list is correctly parsed and stored as IpNet objects
// eslint-disable-next-line @typescript-eslint/dot-notation
expect(connectionManager['deny']).to.deep.equal(expectedIpNets)
})

it('should fail if the connection manager has mismatched connection limit options', async () => {
await expect(
createLibp2p({
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@
"./tracked-map": {
"types": "./dist/src/tracked-map.d.ts",
"import": "./dist/src/tracked-map.js"
},
"./multiaddrToIpNet": {
"types": "./dist/src/multiaddrToIpNet.d.ts",
"import": "./dist/src/multiaddrToIpNet.js"
}
},
"eslintConfig": {
Expand All @@ -155,6 +159,7 @@
"test:electron-main": "aegir test -t electron-main"
},
"dependencies": {
"@chainsafe/netmask": "^2.0.0",
"@chainsafe/is-ip": "^2.0.2",
"@libp2p/crypto": "^5.0.6",
"@libp2p/interface": "^2.2.0",
Expand Down
32 changes: 32 additions & 0 deletions packages/utils/src/multiaddrToIpNet.ts
acul71 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { multiaddr } from '@multiformats/multiaddr'
import { convertToIpNet } from '@multiformats/multiaddr/convert'
import type { IpNet } from '@chainsafe/netmask'
import type { Multiaddr } from '@multiformats/multiaddr'

/**
* Converts a multiaddr string or object to an IpNet object.
* If the multiaddr doesn't include /ipcidr/32, it will encapsulate with /ipcidr/32.
*
* @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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only used in the libp2p module so please define it there in the src/connection-manager/utils.ts file. Please also move the tests to that module.

If we later find that we need it elsewhere we can move it to the utils package, but right now there's no need to expand the public API.

try {
let parsedMa: Multiaddr
if (typeof ma === 'string') {
parsedMa = multiaddr(ma)
} else {
parsedMa = ma
}

// Check if /ipcidr is already present, if not encapsulate with /ipcidr/32
if (!parsedMa.protoNames().includes('ipcidr')) {
parsedMa = parsedMa.encapsulate('/ipcidr/32')
}

return convertToIpNet(parsedMa)
} catch (error) {
throw new Error(`Can't convert to IpNet, Invalid multiaddr format: ${ma}`)
}
}
18 changes: 18 additions & 0 deletions packages/utils/test/multiaddrToIpNet.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-env mocha */

import { expect } from 'aegir/chai'
import { multiaddrToIpNet } from '../src/multiaddrToIpNet.js'

describe('multiaddrToIpNet', () => {
it('should convert a simple multiaddr to an IpNet', () => {
const ma = '/ip4/127.0.0.1'
const ipNet = multiaddrToIpNet(ma)
expect(ipNet.toString()).to.equal('127.0.0.1/32')
})

it('should convert a multiaddr with a ipcidr to an IpNet', () => {
const ma = '/ip4/127.0.0.1/ipcidr/32'
const ipNet = multiaddrToIpNet(ma)
expect(ipNet.toString()).to.equal('127.0.0.1/32')
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you add some tests for IPv6 addresses

})