mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-30 16:10:06 +02:00
add dual-stack check
This commit is contained in:
@@ -8,9 +8,10 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
|
||||
// ASN org-name patterns that strongly suggest a VPN service
|
||||
const VPN_PATTERNS = [
|
||||
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /surfshark/i,
|
||||
/protonvpn/i, /cyberghost/i, /ipvanish/i, /purevpn/i, /tunnelbear/i,
|
||||
/private.?internet.?access/i, /\bpia\b/i, /hide\.?my\.?ip/i, /hidemyass/i,
|
||||
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /31173\s+services/i,
|
||||
/owl\s+limited/i, /surfshark/i, /protonvpn/i, /cyberghost/i, /ipvanish/i,
|
||||
/purevpn/i, /tunnelbear/i, /private.?internet.?access/i, /\bpia\b/i,
|
||||
/hide\.?my\.?ip/i, /hidemyass/i, /windscribe/i, /perfect.?privacy/i,
|
||||
];
|
||||
|
||||
// ASN org-name patterns that suggest cloud/datacenter/hosting (but not necessarily VPN)
|
||||
@@ -58,7 +59,10 @@ router.get('/:ip', async (req, res, next) => {
|
||||
|
||||
// Tor exit-node check (async DNS, ~100–300 ms)
|
||||
const tor = await isTorExit(ip);
|
||||
if (tor) flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
|
||||
if (tor) {
|
||||
flags.length = 0; // Tor supersedes all network-type flags — showing "Residential" alongside Tor is misleading
|
||||
flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
|
||||
}
|
||||
|
||||
logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
|
||||
res.json({ success: true, ip, flags });
|
||||
|
||||
+44
-18
@@ -31,9 +31,21 @@ export const page = {
|
||||
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span>
|
||||
<button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
|
||||
</div>
|
||||
<!-- Privacy / risk flags -->
|
||||
<div id="privacy-flags" class="hidden mt-3 flex flex-wrap gap-1.5"></div>
|
||||
<div id="privacy-loader" class="loader mt-3" style="width:16px;height:16px;border-width:2px"></div>
|
||||
<!-- Privacy / risk flags — dual-stack: always show both rows -->
|
||||
<div id="privacy-checks" class="mt-3 space-y-1.5">
|
||||
<div class="flex items-center gap-2 min-h-[22px]">
|
||||
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider w-10 shrink-0">IPv4</span>
|
||||
<div id="privacy-flags-v4" class="flex flex-wrap gap-1.5 items-center">
|
||||
<div id="privacy-loader-v4" class="loader" style="width:12px;height:12px;border-width:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-h-[22px]">
|
||||
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
|
||||
<div id="privacy-flags-v6" class="flex flex-wrap gap-1.5 items-center">
|
||||
<span class="text-xs text-gray-600 italic">detecting…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card rounded-lg p-5 space-y-4">
|
||||
@@ -476,17 +488,26 @@ export const page = {
|
||||
async function checkIPv6() {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 4000);
|
||||
const v6Flags = document.getElementById('privacy-flags-v6');
|
||||
try {
|
||||
const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal });
|
||||
const ip6 = r.ok ? (await r.text()).trim() : null;
|
||||
if (ip6 && ip6.includes(':')) {
|
||||
if (ip6 && ip6.includes(':') && ip6 !== currentIp) {
|
||||
// Separate IPv6 address — show it and run its own privacy check
|
||||
document.getElementById('ipv6-address').textContent = ip6;
|
||||
const row = document.getElementById('ipv6-row');
|
||||
row.classList.remove('hidden');
|
||||
document.getElementById('ipv6-row').classList.remove('hidden');
|
||||
copyIpv6Btn.classList.remove('hidden');
|
||||
if (v6Flags) v6Flags.innerHTML = '<div id="privacy-loader-v6" class="loader" style="width:12px;height:12px;border-width:2px"></div>';
|
||||
fetchPrivacyFlags(ip6, 6);
|
||||
} else {
|
||||
// No IPv6, timed out, or same exit as IPv4 (VPN tunnels both)
|
||||
if (v6Flags) v6Flags.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||
}
|
||||
} catch {
|
||||
if (v6Flags) v6Flags.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
} catch { /* no IPv6 or timeout — show nothing */ }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
// ── Privacy / risk flags ──────────────────────────────────────
|
||||
@@ -497,19 +518,25 @@ export const page = {
|
||||
red: 'bg-red-900/40 text-red-300 border border-red-700/50',
|
||||
};
|
||||
|
||||
async function fetchPrivacyFlags(ip) {
|
||||
const loader = document.getElementById('privacy-loader');
|
||||
const container = document.getElementById('privacy-flags');
|
||||
async function fetchPrivacyFlags(ip, version = 4) {
|
||||
const container = document.getElementById(`privacy-flags-v${version}`);
|
||||
const loader = document.getElementById(`privacy-loader-v${version}`);
|
||||
if (!container) return;
|
||||
try {
|
||||
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
|
||||
const data = await r.json();
|
||||
if (!data.flags?.length) return;
|
||||
loader?.remove();
|
||||
if (!data.flags?.length) {
|
||||
container.innerHTML = '<span class="text-xs text-gray-600">—</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = data.flags.map(f =>
|
||||
`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${FLAG_COLORS[f.color] || FLAG_COLORS.green}">${f.label}</span>`
|
||||
).join('');
|
||||
container.classList.remove('hidden');
|
||||
} catch { /* silent fail */ }
|
||||
finally { loader?.classList.add('hidden'); }
|
||||
} catch {
|
||||
loader?.remove();
|
||||
container.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Browser fingerprint ───────────────────────────────────────
|
||||
@@ -572,7 +599,7 @@ export const page = {
|
||||
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
||||
const data = await r.json();
|
||||
currentIp = data.ip;
|
||||
fetchPrivacyFlags(data.ip);
|
||||
fetchPrivacyFlags(data.ip, 4);
|
||||
|
||||
ipAddressSpan.textContent = data.ip;
|
||||
ipAddressLink.classList.remove('hidden');
|
||||
@@ -941,8 +968,7 @@ export const page = {
|
||||
|
||||
// ── Bootstrap ────────────────────────────────────────────────
|
||||
populateBrowserFingerprint();
|
||||
checkIPv6();
|
||||
fetchIpInfo();
|
||||
fetchIpInfo().then(() => checkIPv6());
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const ipParam = params.get('ip');
|
||||
|
||||
Reference in New Issue
Block a user