Skip to content

Commit 04c17cf

Browse files
committed
Add local TCP/UDP probe listener and MTR pass/fail output
1 parent a4973b8 commit 04c17cf

2 files changed

Lines changed: 92 additions & 3 deletions

File tree

cli.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const ora = require('ora');
1010
const fs = require('fs');
1111
const path = require('path');
1212
const http = require('http');
13+
const net = require('net');
14+
const dgram = require('dgram');
1315
const { spawnSync } = require('child_process');
1416
const IranConnectivityAnalyzer = require('./iran_connectivity');
1517
const { TunnelRecommendationEngine } = require('./tunnel_recommendations');
@@ -354,6 +356,13 @@ class IranCheckCLI {
354356
}
355357
}
356358
async printDetailedReport(results) {
359+
const mtrBadge = (connection) => {
360+
if (!connection) return '✗';
361+
if (!connection.mtrAvailable) return '✗';
362+
if (typeof connection.mtrLossPercent !== 'number') return '✗';
363+
return connection.mtrLossPercent <= 20 ? '✓' : '✗';
364+
};
365+
357366
console.log(chalk.blue('\n📊 Detailed report:'));
358367
console.log(chalk.gray('═'.repeat(80)));
359368
// Summary statistics
@@ -366,7 +375,9 @@ class IranCheckCLI {
366375
if (results.successfulProviders.length > 0) {
367376
console.log(chalk.green('\n✅ Successful providers:'));
368377
results.successfulProviders.forEach((provider, index) => {
369-
console.log(` ${index + 1}. ${provider.name} (score: ${provider.bestScore})`);
378+
const badge = mtrBadge(provider.bestConnection);
379+
const loss = typeof provider.bestConnection?.mtrLossPercent === 'number' ? `${provider.bestConnection.mtrLossPercent}%` : 'N/A';
380+
console.log(` ${index + 1}. ${provider.name} (MTR: ${badge}, loss: ${loss})`);
370381
});
371382
}
372383
// Failed providers
@@ -383,7 +394,9 @@ class IranCheckCLI {
383394
if (provider.successfulConnections.length > 0) {
384395
console.log(chalk.green(`\n ${provider.name}:`));
385396
console.log(` • Successful paths: ${provider.successfulConnections.length}`);
386-
console.log(` • Best score: ${provider.connectivityScore}`);
397+
const badge = mtrBadge(provider.bestConnection);
398+
const loss = typeof provider.bestConnection?.mtrLossPercent === 'number' ? `${provider.bestConnection.mtrLossPercent}%` : 'N/A';
399+
console.log(` • MTR test result: ${badge} (loss: ${loss})`);
387400
const groupedByRange = provider.successfulConnections.reduce((acc, conn) => {
388401
const range = conn.testedCidr || 'Unknown range';
389402
if (!acc[range]) acc[range] = 0;
@@ -428,6 +441,7 @@ class IranCheckCLI {
428441
console.log(` • BGP prefix: ${conn.bgpPrefix ?? 'N/A'}`);
429442
console.log(` • BGP registry: ${conn.bgpRegistry ?? 'N/A'}`);
430443
}
444+
console.log(` • Stage 4 (MTR): ${conn.stageResults?.mtr || 'skipped'}`);
431445
console.log(` • Target reachability: ${conn.targetReachability || 0}%`);
432446
}
433447
}
@@ -606,6 +620,46 @@ class IranCheckCLI {
606620
console.log(chalk.cyan(`🧾 Total providers: ${providers.length}`));
607621
}
608622
}
623+
624+
function startProbeListener(options = {}) {
625+
const host = options.host || '0.0.0.0';
626+
const port = Number(options.port || 9000);
627+
const protocol = String(options.protocol || 'both').toLowerCase();
628+
const tcpEnabled = protocol === 'tcp' || protocol === 'both';
629+
const udpEnabled = protocol === 'udp' || protocol === 'both';
630+
631+
if (!tcpEnabled && !udpEnabled) {
632+
throw new Error('Protocol must be tcp, udp, or both');
633+
}
634+
635+
if (tcpEnabled) {
636+
const tcpServer = net.createServer((socket) => {
637+
socket.setEncoding('utf8');
638+
socket.on('data', (data) => {
639+
const payload = String(data || '').trim();
640+
socket.write(`OK TCP port=${port} payload="${payload || 'empty'}"\n`);
641+
});
642+
socket.on('error', () => {});
643+
});
644+
tcpServer.listen(port, host, () => {
645+
console.log(chalk.green(`✅ TCP listener active on ${host}:${port}`));
646+
});
647+
}
648+
649+
if (udpEnabled) {
650+
const udpServer = dgram.createSocket('udp4');
651+
udpServer.on('message', (msg, rinfo) => {
652+
const payload = String(msg || '').trim();
653+
const response = Buffer.from(`OK UDP port=${port} payload="${payload || 'empty'}"`);
654+
udpServer.send(response, rinfo.port, rinfo.address);
655+
});
656+
udpServer.bind(port, host, () => {
657+
console.log(chalk.green(`✅ UDP listener active on ${host}:${port}`));
658+
});
659+
}
660+
661+
console.log(chalk.cyan(`ℹ️ Listener running (protocol=${protocol}). Press Ctrl+C to stop.`));
662+
}
609663
// Commander.js setup
610664
program
611665
.name('iran-check')
@@ -637,6 +691,16 @@ program
637691
banner: options.banner
638692
});
639693
});
694+
program
695+
.command('listener')
696+
.description('Start local TCP/UDP echo listener for Iranian-side port/protocol testing')
697+
.option('--host <host>', 'Bind host', '0.0.0.0')
698+
.option('--port <port>', 'Bind port', '9000')
699+
.option('--protocol <protocol>', 'tcp | udp | both', 'both')
700+
.action((options) => {
701+
startProbeListener(options);
702+
});
703+
640704
program
641705
.command('providers')
642706
.description('Show provider list')

iran_connectivity.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,17 @@ ${error.stderr || ''}`;
110110
bgpOriginAsn: null,
111111
bgpPrefix: null,
112112
bgpRegistry: null,
113+
mtrAvailable: false,
114+
mtrLossPercent: null,
115+
mtrRawSample: null,
113116
responseTime: 0,
114117
targetReachability: 0,
115118
connectivityScore: 0,
116119
stageResults: {
117120
ping: 'failed',
118121
traceroute: 'skipped',
119-
bgp: 'skipped'
122+
bgp: 'skipped',
123+
mtr: 'skipped'
120124
},
121125
sourcePingStats: null,
122126
targetPingStats: null
@@ -183,6 +187,27 @@ ${error.stderr || ''}`;
183187
} else {
184188
results.stageResults.bgp = 'unavailable';
185189
}
190+
191+
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"`);
192+
const mtrOutput = mtr.output || '';
193+
if (!mtrOutput.toLowerCase().includes('mtr_unavailable')) {
194+
results.mtrAvailable = true;
195+
const mtrLines = mtrOutput
196+
.split('\n')
197+
.map((line) => line.trim())
198+
.filter((line) => line && !line.toLowerCase().startsWith('start:') && !line.toLowerCase().startsWith('host:') && !line.toLowerCase().includes('loss%'));
199+
const lastHop = mtrLines[mtrLines.length - 1] || '';
200+
results.mtrRawSample = lastHop || null;
201+
const lossMatch = lastHop.match(/(\d+(?:\.\d+)?)%/);
202+
if (lossMatch) {
203+
results.mtrLossPercent = Number(lossMatch[1]);
204+
results.stageResults.mtr = results.mtrLossPercent <= 20 ? 'passed' : 'failed';
205+
} else {
206+
results.stageResults.mtr = 'no-data';
207+
}
208+
} else {
209+
results.stageResults.mtr = 'unavailable';
210+
}
186211
}
187212

188213
results.responseTime = Date.now() - startTime;

0 commit comments

Comments
 (0)