@projectlibertylabs/p2p-peer-test
Version:
CLI tool to test libp2p connections and discover peer protocols
241 lines (213 loc) • 8.14 kB
JavaScript
import { createLibp2p } from 'libp2p';
import { webSockets } from '@libp2p/websockets';
import { tcp } from '@libp2p/tcp';
import { identify } from '@libp2p/identify';
import { ping } from '@libp2p/ping';
import { noise } from '@chainsafe/libp2p-noise';
import { yamux } from '@chainsafe/libp2p-yamux';
import { multiaddr } from '@multiformats/multiaddr';
import { DEFAULT_CONFIG, ERROR_MESSAGES } from './config.js';
import { createPromiseWithTimeout, measureDuration } from './utils.js';
/**
* Creates a libp2p node with standard configuration
* @returns {Promise<Object>} Configured libp2p node instance
*/
const createLibp2pNode = () =>
createLibp2p({
addresses: { listen: DEFAULT_CONFIG.LISTEN_ADDRESSES },
transports: [webSockets(), tcp()],
connectionEncrypters: [noise()],
streamMuxers: [yamux()],
services: {
identify: identify(),
ping: ping({ timeout: DEFAULT_CONFIG.IDENTIFY_TIMEOUT })
},
connectionManager: { maxConnections: DEFAULT_CONFIG.CONNECTION_MAX }
});
/**
* Discovers protocols supported by the connected peer
* @param {Object} node - The libp2p node instance
* @param {Object} connection - The active connection to the peer
* @returns {Promise<string[]>} Array of protocol strings supported by the peer
*/
const discoverProtocols = async (node, connection) => {
// Try peer store first
const peerStoreProtocols = await tryGetPeerStoreProtocols(node, connection);
if (peerStoreProtocols.length > 0) return peerStoreProtocols;
// Try connection streams
const streamProtocols = tryGetStreamProtocols(connection);
if (streamProtocols.length > 0) return streamProtocols;
// Fallback based on connection type
return getFallbackProtocols(connection);
};
/**
* Attempts to get protocols from the peer store
* @param {Object} node - The libp2p node instance
* @param {Object} connection - The active connection to the peer
* @returns {Promise<string[]>} Array of protocols from peer store or empty array
*/
const tryGetPeerStoreProtocols = async (node, connection) => {
try {
await new Promise((resolve) => setTimeout(resolve, DEFAULT_CONFIG.PROTOCOL_DISCOVERY_DELAY));
const peer = await node.peerStore.get(connection.remotePeer);
return Array.from(peer.protocols || []);
} catch {
return [];
}
};
/**
* Attempts to get protocols from connection streams
* @param {Object} connection - The active connection to the peer
* @returns {string[]} Array of protocols from streams or empty array
*/
const tryGetStreamProtocols = (connection) => {
try {
const streams = connection.streams;
return streams && streams.length > 0
? streams.map((stream) => stream.protocol || 'unknown')
: [];
} catch {
return [];
}
};
/**
* Provides fallback protocols based on connection type
* @param {Object} connection - The active connection to the peer
* @returns {string[]} Array with fallback protocol based on connection transport
*/
const getFallbackProtocols = (connection) => [
connection.remoteAddr.toString().includes('ws') ? 'libp2p/websocket' : 'libp2p/tcp'
];
/**
* Extracts the expected peer ID from a multiaddr string
* @param {string} multiaddrStr - The multiaddr string
* @returns {string|null} The expected peer ID or null if not specified
*/
export function extractExpectedPeerId(multiaddrStr) {
const p2pMatch = multiaddrStr.match(/\/p2p\/([^/]+)/);
return p2pMatch ? p2pMatch[1] : null;
}
/**
* Tests a libp2p connection to a given multiaddr
* @param {string} multiaddrStr - The multiaddr string to connect to
* @param {number} [timeoutMs=DEFAULT_CONFIG.TIMEOUT_MS] - Connection timeout in milliseconds
* @returns {Promise<Object>} Connection result object with success status, protocols, peerId, duration, and error details
*/
export async function testLibp2pConnection(multiaddrStr, timeoutMs = DEFAULT_CONFIG.TIMEOUT_MS) {
let node = null;
const startTime = performance.now();
try {
const addr = multiaddr(multiaddrStr);
const expectedPeerId = extractExpectedPeerId(multiaddrStr);
node = await createLibp2pNode();
await node.start();
// Use utility function instead of Promise.race
const connection = await createPromiseWithTimeout(
node.dial(addr),
timeoutMs,
ERROR_MESSAGES.CONNECTION_TIMEOUT(timeoutMs)
);
const protocols = await discoverProtocols(node, connection);
const actualPeerId = connection.remotePeer.toString();
const duration = measureDuration(startTime);
await connection.close();
await node.stop();
// Check if peer ID matches expected (if specified in multiaddr)
if (expectedPeerId && actualPeerId !== expectedPeerId) {
return {
success: false,
protocols: [],
peerId: actualPeerId,
expectedPeerId,
error: `Peer ID mismatch: expected ${expectedPeerId}, got ${actualPeerId}`,
details: 'Connected to wrong peer - check the multiaddr peer ID component',
duration
};
}
return {
success: true,
protocols,
peerId: actualPeerId,
duration
};
} catch (error) {
if (node) {
try {
console.error('Connection Failure', error);
await node.stop();
} catch (e) {
console.warn('Connection cleanup failure', e);
}
}
const errorMessage = error instanceof Error ? error.message : String(error);
const duration = measureDuration(startTime);
return {
success: false,
protocols: [],
error: errorMessage,
details: getErrorDetails(errorMessage),
duration
};
}
}
/**
* Provides detailed error messages and hints based on error patterns
* @param {string} errorMessage - The error message to analyze
* @returns {string} Detailed error description with troubleshooting hints
*/
function getErrorDetails(errorMessage) {
// Network errors
if (errorMessage.includes('ECONNREFUSED')) {
return 'Connection refused - check if the node is running and the port is correct';
}
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ENOTREACHED')) {
return 'Host not found - check the hostname/IP address';
}
if (errorMessage.includes('EHOSTUNREACH')) {
return 'Host unreachable - check network connectivity and firewall settings';
}
if (errorMessage.includes('ENETUNREACH')) {
return 'Network unreachable - check your internet connection';
}
// Timeout errors
if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
return 'Connection timeout - the node may be unreachable or overloaded';
}
// Protocol errors
if (errorMessage.includes('protocol') || errorMessage.includes('handshake')) {
return 'Protocol mismatch - ensure the node supports libp2p connections';
}
if (errorMessage.includes('noise') || errorMessage.includes('encryption')) {
return 'Encryption handshake failed - the node may not support Noise protocol';
}
if (errorMessage.includes('yamux') || errorMessage.includes('mux')) {
return 'Stream multiplexing failed - the node may not support Yamux';
}
// Transport errors
if (errorMessage.includes('websocket') || errorMessage.includes('ws')) {
return 'WebSocket connection failed - check WebSocket support and network connectivity';
}
if (errorMessage.includes('tcp')) {
return 'TCP connection failed - check network connectivity and firewall settings';
}
// DNS resolution errors
if (errorMessage.includes('dns4') || errorMessage.includes('dns6')) {
return 'DNS resolution failed - check hostname or try using IP address instead';
}
// Multiaddr errors
if (errorMessage.includes('multiaddr') || errorMessage.includes('invalid address')) {
return 'Invalid multiaddr format - check the syntax and protocol components';
}
// Peer errors
if (errorMessage.includes('peer') || errorMessage.includes('identify')) {
return 'Peer identification failed - the remote node may not be a libp2p node';
}
return 'Unknown connection error - check logs for more details';
}
export const EXAMPLE_MULTIADDRS = [
'/ip4/127.0.0.1/tcp/30333',
'/ip4/127.0.0.1/tcp/30334',
'/ip4/127.0.0.1/tcp/30333/ws',
'/dns/bootnode-0.polkadot.io/tcp/30333/p2p/12D3KooWEdsXX9ejydkzw6nt4jKBrGg8T5zwcKC5TXv2RUNt5khz',
'/dns/bootnode-1.polkadot.io/tcp/30333/p2p/12D3KooWAtJF1vTYdgBbHmIajSFHwtZuq8Xza1Fb6Lwsb4r4gN5p'
];