add dual-stack check

This commit is contained in:
2026-05-17 21:24:10 +02:00
parent ea0d192365
commit 014d1704de
2 changed files with 54 additions and 24 deletions
+8 -4
View File
@@ -8,9 +8,10 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
// ASN org-name patterns that strongly suggest a VPN service // ASN org-name patterns that strongly suggest a VPN service
const VPN_PATTERNS = [ const VPN_PATTERNS = [
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /surfshark/i, /\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /31173\s+services/i,
/protonvpn/i, /cyberghost/i, /ipvanish/i, /purevpn/i, /tunnelbear/i, /owl\s+limited/i, /surfshark/i, /protonvpn/i, /cyberghost/i, /ipvanish/i,
/private.?internet.?access/i, /\bpia\b/i, /hide\.?my\.?ip/i, /hidemyass/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) // 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, ~100300 ms) // Tor exit-node check (async DNS, ~100300 ms)
const tor = await isTorExit(ip); 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'); logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
res.json({ success: true, ip, flags }); res.json({ success: true, ip, flags });
+46 -20
View File
@@ -31,9 +31,21 @@ export const page = {
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span> <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> <button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
</div> </div>
<!-- Privacy / risk flags --> <!-- Privacy / risk flags — dual-stack: always show both rows -->
<div id="privacy-flags" class="hidden mt-3 flex flex-wrap gap-1.5"></div> <div id="privacy-checks" class="mt-3 space-y-1.5">
<div id="privacy-loader" class="loader mt-3" style="width:16px;height:16px;border-width:2px"></div> <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>
<div class="glass-card rounded-lg p-5 space-y-4"> <div class="glass-card rounded-lg p-5 space-y-4">
@@ -474,19 +486,28 @@ export const page = {
// ── IPv6 detection ─────────────────────────────────────────── // ── IPv6 detection ───────────────────────────────────────────
async function checkIPv6() { async function checkIPv6() {
const ctrl = new AbortController(); const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 4000); const timer = setTimeout(() => ctrl.abort(), 4000);
const v6Flags = document.getElementById('privacy-flags-v6');
try { try {
const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal }); const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal });
const ip6 = r.ok ? (await r.text()).trim() : null; 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; document.getElementById('ipv6-address').textContent = ip6;
const row = document.getElementById('ipv6-row'); document.getElementById('ipv6-row').classList.remove('hidden');
row.classList.remove('hidden');
copyIpv6Btn.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 { /* no IPv6 or timeout — show nothing */ } } catch {
finally { clearTimeout(timer); } if (v6Flags) v6Flags.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
} finally {
clearTimeout(timer);
}
} }
// ── Privacy / risk flags ────────────────────────────────────── // ── Privacy / risk flags ──────────────────────────────────────
@@ -497,19 +518,25 @@ export const page = {
red: 'bg-red-900/40 text-red-300 border border-red-700/50', red: 'bg-red-900/40 text-red-300 border border-red-700/50',
}; };
async function fetchPrivacyFlags(ip) { async function fetchPrivacyFlags(ip, version = 4) {
const loader = document.getElementById('privacy-loader'); const container = document.getElementById(`privacy-flags-v${version}`);
const container = document.getElementById('privacy-flags'); const loader = document.getElementById(`privacy-loader-v${version}`);
if (!container) return;
try { try {
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`); const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
const data = await r.json(); 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 => 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>` `<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(''); ).join('');
container.classList.remove('hidden'); } catch {
} catch { /* silent fail */ } loader?.remove();
finally { loader?.classList.add('hidden'); } container.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
}
} }
// ── Browser fingerprint ─────────────────────────────────────── // ── Browser fingerprint ───────────────────────────────────────
@@ -572,7 +599,7 @@ export const page = {
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`); if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
const data = await r.json(); const data = await r.json();
currentIp = data.ip; currentIp = data.ip;
fetchPrivacyFlags(data.ip); fetchPrivacyFlags(data.ip, 4);
ipAddressSpan.textContent = data.ip; ipAddressSpan.textContent = data.ip;
ipAddressLink.classList.remove('hidden'); ipAddressLink.classList.remove('hidden');
@@ -941,8 +968,7 @@ export const page = {
// ── Bootstrap ──────────────────────────────────────────────── // ── Bootstrap ────────────────────────────────────────────────
populateBrowserFingerprint(); populateBrowserFingerprint();
checkIPv6(); fetchIpInfo().then(() => checkIPv6());
fetchIpInfo();
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
const ipParam = params.get('ip'); const ipParam = params.get('ip');