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
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,41 @@ node cli.js config add-range "192.168.1.0/24" "Custom Provider"
node cli.js config reset
```

### 4. Iran-side Local Listener (No extra package) | لیسنر محلی سمت ایران (بدون پکیج اضافه)

**English:**
```bash
# Start TCP + UDP listener on port 9000 (default)
node cli.js listener

# Start only TCP on a custom port
node cli.js listener --protocol tcp --port 8081

# Start only UDP on a custom host/port
node cli.js listener --protocol udp --host 0.0.0.0 --port 5353
```

**فارسی:**
```bash
# اجرای لیسنر TCP و UDP روی پورت ۹۰۰۰ (پیشفرض)
node cli.js listener

# فقط TCP روی پورت دلخواه
node cli.js listener --protocol tcp --port 8081

# فقط UDP روی هاست/پورت دلخواه
node cli.js listener --protocol udp --host 0.0.0.0 --port 5353
```

**Quick probe examples | نمونه تست سریع**
```bash
# TCP test (from client machine)
echo "ping" | nc <IRAN_SERVER_IP> 9000

# UDP test (from client machine)
echo "ping" | nc -u -w2 <IRAN_SERVER_IP> 9000
```

---

## 📊 Features | ویژگی‌ها
Expand All @@ -182,13 +217,13 @@ node cli.js config reset
- **Port Testing**: 80, 443, 22, 53
- **Connection Quality**: Response time, packet loss, stability
- **Network Analysis**: Firewall detection, routing paths
- **Scoring System**: 0-100 connectivity score
- **MTR Result**: clear ✓ / ✗ status in detailed output (+ loss %)

**فارسی:**
- **تست پورت**: ۸۰، ۴۴۳، ۲۲، ۵۳
- **کیفیت اتصال**: زمان پاسخ، از دست دادن بسته، پایداری
- **تحلیل شبکه**: تشخیص فایروال، مسیرهای مسیریابی
- **سیستم امتیازدهی**: امتیاز اتصال ۰-۱۰۰
- **نتیجه MTR**: نمایش واضح ✓ / ✗ در خروجی دقیق (به‌همراه درصد loss)

### Tunnel Recommendations | پیشنهادهای تونل‌زنی

Expand Down Expand Up @@ -229,6 +264,7 @@ node cli.js config reset
| Command | Description | توضیح فارسی |
|---------|-------------|-------------|
| `analyze <IP>` | Analyze connectivity to Iranian IP | تحلیل اتصال به IP ایرانی |
| `listener` | Start local TCP/UDP probe listener | اجرای لیسنر محلی TCP/UDP برای تست |
| `providers` | List all providers | لیست تمام ارائه‌دهندگان |
| `recommend <file>` | Get tunnel recommendations | دریافت پیشنهادهای تونل‌زنی |
| `config` | Configuration management | مدیریت پیکربندی |
Expand Down Expand Up @@ -287,7 +323,7 @@ done
**English:**
```bash
# Pipe to other tools
node cli.js analyze 185.185.123.45 --export json | jq '.results[] | select(.score > 70)'
node cli.js analyze 185.185.123.45 --export json | jq '.results[] | select(.ports["443"] == true)'

# Use with cron for monitoring
echo "0 2 * * * cd /path/to/irancheck && node cli.js analyze 185.185.123.45 --export json --output daily_\$(date +\%Y\%m\%d).json" | crontab -
Expand All @@ -296,7 +332,7 @@ echo "0 2 * * * cd /path/to/irancheck && node cli.js analyze 185.185.123.45 --ex
**فارسی:**
```bash
# اتصال به ابزارهای دیگر
node cli.js analyze 185.185.123.45 --export json | jq '.results[] | select(.score > 70)'
node cli.js analyze 185.185.123.45 --export json | jq '.results[] | select(.ports["443"] == true)'

# استفاده با cron برای نظارت
echo "0 2 * * * cd /path/to/irancheck && node cli.js analyze 185.185.123.45 --export json --output daily_\$(date +\%Y\%m\%d).json" | crontab -
Expand Down
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep bestConnection when rendering MTR provider summary

In printDetailedReport, the successful-provider summary now derives the MTR badge/loss from provider.bestConnection, but entries in results.successfulProviders are populated without a bestConnection field (they only carry provider/name/count/score in runAnalysis). When --detailed output is shown, this makes every successful provider display MTR: ✗, loss: N/A regardless of real MTR results, so the newly added MTR summary is systematically incorrect.

Useful? React with 👍 / 👎.

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