Skip to content
Merged
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
68 changes: 66 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const ora = require('ora');
const fs = require('fs');
const path = require('path');
const http = require('http');
const net = require('net');
const dgram = require('dgram');
const { spawnSync } = require('child_process');
const IranConnectivityAnalyzer = require('./iran_connectivity');
const { TunnelRecommendationEngine } = require('./tunnel_recommendations');
Expand Down Expand Up @@ -354,6 +356,13 @@ class IranCheckCLI {
}
}
async printDetailedReport(results) {
const mtrBadge = (connection) => {
if (!connection) return '✗';
if (!connection.mtrAvailable) return '✗';
if (typeof connection.mtrLossPercent !== 'number') return '✗';
return connection.mtrLossPercent <= 20 ? '✓' : '✗';
};

console.log(chalk.blue('\n📊 Detailed report:'));
console.log(chalk.gray('═'.repeat(80)));
// Summary statistics
Expand All @@ -366,7 +375,9 @@ class IranCheckCLI {
if (results.successfulProviders.length > 0) {
console.log(chalk.green('\n✅ Successful providers:'));
results.successfulProviders.forEach((provider, index) => {
console.log(` ${index + 1}. ${provider.name} (score: ${provider.bestScore})`);
const badge = mtrBadge(provider.bestConnection);
const loss = typeof provider.bestConnection?.mtrLossPercent === 'number' ? `${provider.bestConnection.mtrLossPercent}%` : 'N/A';
console.log(` ${index + 1}. ${provider.name} (MTR: ${badge}, loss: ${loss})`);
});
}
// Failed providers
Expand All @@ -383,7 +394,9 @@ class IranCheckCLI {
if (provider.successfulConnections.length > 0) {
console.log(chalk.green(`\n ${provider.name}:`));
console.log(` • Successful paths: ${provider.successfulConnections.length}`);
console.log(` • Best score: ${provider.connectivityScore}`);
const badge = mtrBadge(provider.bestConnection);
const loss = typeof provider.bestConnection?.mtrLossPercent === 'number' ? `${provider.bestConnection.mtrLossPercent}%` : 'N/A';
console.log(` • MTR test result: ${badge} (loss: ${loss})`);
const groupedByRange = provider.successfulConnections.reduce((acc, conn) => {
const range = conn.testedCidr || 'Unknown range';
if (!acc[range]) acc[range] = 0;
Expand Down Expand Up @@ -428,6 +441,7 @@ class IranCheckCLI {
console.log(` • BGP prefix: ${conn.bgpPrefix ?? 'N/A'}`);
console.log(` • BGP registry: ${conn.bgpRegistry ?? 'N/A'}`);
}
console.log(` • Stage 4 (MTR): ${conn.stageResults?.mtr || 'skipped'}`);
console.log(` • Target reachability: ${conn.targetReachability || 0}%`);
}
}
Expand Down Expand Up @@ -606,6 +620,46 @@ class IranCheckCLI {
console.log(chalk.cyan(`🧾 Total providers: ${providers.length}`));
}
}

function startProbeListener(options = {}) {
const host = options.host || '0.0.0.0';
const port = Number(options.port || 9000);
const protocol = String(options.protocol || 'both').toLowerCase();
const tcpEnabled = protocol === 'tcp' || protocol === 'both';
const udpEnabled = protocol === 'udp' || protocol === 'both';

if (!tcpEnabled && !udpEnabled) {
throw new Error('Protocol must be tcp, udp, or both');
}

if (tcpEnabled) {
const tcpServer = net.createServer((socket) => {
socket.setEncoding('utf8');
socket.on('data', (data) => {
const payload = String(data || '').trim();
socket.write(`OK TCP port=${port} payload="${payload || 'empty'}"\n`);
});
socket.on('error', () => {});
});
tcpServer.listen(port, host, () => {
console.log(chalk.green(`✅ TCP listener active on ${host}:${port}`));
});
}

if (udpEnabled) {
const udpServer = dgram.createSocket('udp4');
udpServer.on('message', (msg, rinfo) => {
const payload = String(msg || '').trim();
const response = Buffer.from(`OK UDP port=${port} payload="${payload || 'empty'}"`);
udpServer.send(response, rinfo.port, rinfo.address);
});
udpServer.bind(port, host, () => {
console.log(chalk.green(`✅ UDP listener active on ${host}:${port}`));
});
}

console.log(chalk.cyan(`ℹ️ Listener running (protocol=${protocol}). Press Ctrl+C to stop.`));
}
// Commander.js setup
program
.name('iran-check')
Expand Down Expand Up @@ -637,6 +691,16 @@ program
banner: options.banner
});
});
program
.command('listener')
.description('Start local TCP/UDP echo listener for Iranian-side port/protocol testing')
.option('--host <host>', 'Bind host', '0.0.0.0')
.option('--port <port>', 'Bind port', '9000')
.option('--protocol <protocol>', 'tcp | udp | both', 'both')
.action((options) => {
startProbeListener(options);
});

program
.command('providers')
.description('Show provider list')
Expand Down
27 changes: 26 additions & 1 deletion iran_connectivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,17 @@ ${error.stderr || ''}`;
bgpOriginAsn: null,
bgpPrefix: null,
bgpRegistry: null,
mtrAvailable: false,
mtrLossPercent: null,
mtrRawSample: null,
responseTime: 0,
targetReachability: 0,
connectivityScore: 0,
stageResults: {
ping: 'failed',
traceroute: 'skipped',
bgp: 'skipped'
bgp: 'skipped',
mtr: 'skipped'
},
sourcePingStats: null,
targetPingStats: null
Expand Down Expand Up @@ -183,6 +187,27 @@ ${error.stderr || ''}`;
} else {
results.stageResults.bgp = 'unavailable';
}

const mtr = await this.runShellCheck(`command -v mtr >/dev/null 2>&1 && mtr -n -r -c 3 --report ${finalTarget} 2>/dev/null || echo "MTR_UNAVAILABLE"`);
const mtrOutput = mtr.output || '';
if (!mtrOutput.toLowerCase().includes('mtr_unavailable')) {
results.mtrAvailable = true;
const mtrLines = mtrOutput
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.toLowerCase().startsWith('start:') && !line.toLowerCase().startsWith('host:') && !line.toLowerCase().includes('loss%'));
const lastHop = mtrLines[mtrLines.length - 1] || '';
results.mtrRawSample = lastHop || null;
const lossMatch = lastHop.match(/(\d+(?:\.\d+)?)%/);
if (lossMatch) {
results.mtrLossPercent = Number(lossMatch[1]);
results.stageResults.mtr = results.mtrLossPercent <= 20 ? 'passed' : 'failed';
} else {
results.stageResults.mtr = 'no-data';
}
} else {
results.stageResults.mtr = 'unavailable';
}
}

results.responseTime = Date.now() - startTime;
Expand Down
Loading