fix(tracer): resolve discord community cidr filter (#50) and improve leak check display

This commit is contained in:
Dushnilin
2026-09-14 06:27:31 +03:00
parent 959bcb59f5
commit ea0de78cca
5 changed files with 463 additions and 170 deletions
@@ -128,7 +128,7 @@ export function renderLeakCheckModal() {
const ipAlertClass = !ip_leak.proxy_online
? 'alert-message info'
: isDirectRouting
? 'alert-message info'
? 'alert-message warning'
: 'alert-message success';
const ipAlertText = !ip_leak.proxy_online
@@ -266,32 +266,36 @@ export function renderLeakCheckModal() {
);
// --- 2. DNS Leak Section ---
const dnsAlertClass =
!dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0
? 'alert-message info'
: dns_leak.dns_leaked
? 'alert-message warning'
: 'alert-message success';
const proxyDnsServers = dns_leak.dns_servers || [];
const directDnsServers = dns_leak.direct_dns_servers || [];
const hasProxyDns = proxyDnsServers.length > 0;
const dnsAlertText =
!dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0
const dnsAlertClass = !hasProxyDns
? 'alert-message info'
: dns_leak.dns_leaked
? 'alert-message warning'
: 'alert-message success';
const dnsAlertText = !hasProxyDns
? _(
'DNS resolvers through proxy are not captured (proxy is offline or test domain is not intercepted).',
)
: dns_leak.dns_leaked
? _(
'No DNS resolvers captured through proxy (proxy is offline or test domain is not intercepted).',
'ℹ️ ISP DNS detected: DNS queries are handled by your local Internet Service Provider. If you use selective routing, this is standard behavior for direct connections.',
)
: dns_leak.dns_leaked
? _(
'ℹ️ ISP DNS detected: DNS queries are handled by your local Internet Service Provider. If you use selective routing, this is standard behavior for direct connections.',
)
: _(
'🛡️ SECURE: All DNS queries are resolved through independent secure DNS servers.',
);
: _(
'🛡️ SECURE: All DNS queries are resolved through independent secure DNS servers.',
);
const dnsTableRows = (dns_leak.dns_servers || []).map((s) =>
const makeDnsRow = (s: Tachyon.DNSResolverInfo, pathLabel: string) =>
E('tr', { class: 'tr cbi-section-table-row' }, [
E('td', { class: 'td' }, [E('code', {}, s.ip)]),
E('td', { class: 'td' }, s.country || '—'),
E('td', { class: 'td' }, s.isp || '—'),
E('td', { class: 'td', style: 'text-align: center;' }, [
E(
'td',
{ class: 'td', style: 'text-align: center;' },
s.is_isp
? E(
'span',
@@ -311,9 +315,24 @@ export function renderLeakCheckModal() {
},
_('SAFE'),
),
]),
]),
),
E(
'td',
{
class: 'td',
style: 'font-size: 10px; color: var(--text-color-medium, #6c757d);',
},
pathLabel,
),
]);
const proxyDnsRows = proxyDnsServers.map((s) =>
makeDnsRow(s, _('via Proxy')),
);
const directDnsRows = directDnsServers.map((s) =>
makeDnsRow(s, _('via WAN')),
);
const allDnsRows = [...proxyDnsRows, ...directDnsRows];
const dnsTable = E(
'table',
@@ -332,20 +351,21 @@ export function renderLeakCheckModal() {
{ class: 'th', style: 'text-align: center;' },
_('Verdict'),
),
E('th', { class: 'th' }, _('Path')),
]),
]),
E(
'tbody',
{},
dnsTableRows.length > 0
? dnsTableRows
allDnsRows.length > 0
? allDnsRows
: [
E('tr', { class: 'tr' }, [
E(
'td',
{
class: 'td',
colSpan: 4,
colSpan: 5,
style: 'text-align: center; opacity: 0.7;',
},
_('No DNS resolvers recorded'),
@@ -1,23 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { beforeEach, describe, expect, it, vi } from 'vitest';
// ---------------------------------------------------------------------------
// Hoisted mocks — must come before any imports that transitively use globals
// ---------------------------------------------------------------------------
const mocks = vi.hoisted(() => {
class MockMutationObserver {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
}
(globalThis as any).MutationObserver = MockMutationObserver;
function createDummyElement(tag: string) {
return {
// Minimal DOM mock that tracks tree structure for inspection
function createEl(tag: string) {
const el: any = {
tagName: tag.toUpperCase(),
children: [] as any[],
style: {} as Record<string, string>,
textContent: '',
innerHTML: '',
class: '',
colSpan: 0,
setAttribute() {},
getAttribute() {
return null;
@@ -25,27 +22,28 @@ const mocks = vi.hoisted(() => {
addEventListener() {},
querySelectorAll: () => [],
querySelector: () => null,
replaceChildren: function (...nodes: any[]) {
replaceChildren(...nodes: any[]) {
(this as any).children = nodes;
},
appendChild: function (node: any) {
(this as any).children.push(node);
appendChild(node: any) {
if (node && typeof node === 'object') {
(this as any).children.push(node);
} else if (node) {
(this as any).textContent += String(node);
}
},
append: function (...nodes: any[]) {
nodes.forEach((node) => (this as any).children.push(node));
},
classList: {
add() {},
remove() {},
append(...nodes: any[]) {
nodes.forEach((n) => (this as any).appendChild(n));
},
classList: { add() {}, remove() {} },
};
return el;
}
(globalThis as any).document = {
body: {} as any,
createElement: (tag: string) => createDummyElement(tag) as any,
createElementNS: (_ns: string, tag: string) =>
createDummyElement(tag) as any,
createElement: (tag: string) => createEl(tag),
createElementNS: (_ns: string, tag: string) => createEl(tag),
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
@@ -59,18 +57,16 @@ const mocks = vi.hoisted(() => {
if (style && typeof style === 'string') {
style.split(';').forEach((rule: string) => {
const [k, v] = rule.split(':').map((s: string) => s.trim());
if (k && v) {
el.style[k] = v;
}
if (k && v) el.style[k] = v;
});
} else if (style && typeof style === 'object') {
Object.assign(el.style, style);
}
}
if (children) {
if (children !== undefined && children !== null) {
if (Array.isArray(children)) {
children.forEach((c) => {
if (c) el.appendChild(c);
if (c != null) el.appendChild(c);
});
} else {
el.appendChild(children);
@@ -80,74 +76,125 @@ const mocks = vi.hoisted(() => {
};
(globalThis as any)._ = (str: string) => str;
(globalThis as any).rpc = { declare: vi.fn() };
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
class MockMutationObserver {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
}
(globalThis as any).MutationObserver = MockMutationObserver;
const showModal = vi.fn();
const hideModal = vi.fn();
(globalThis as any).ui = {
showModal,
hideModal,
addNotification: vi.fn(),
};
(globalThis as any).ui = { showModal, hideModal, addNotification: vi.fn() };
return {
showModal,
hideModal,
};
return { showModal, hideModal };
});
// ---------------------------------------------------------------------------
// Imports after mocks
// ---------------------------------------------------------------------------
import { renderLeakCheckModal } from '../partials/renderLeakCheckModal';
import { TachyonShellMethods } from '../../../methods/shell';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Recursively collect all text content from a mock element tree. */
function collectText(node: any): string {
if (!node || typeof node !== 'object') return String(node ?? '');
let text = node.textContent || '';
for (const child of node.children || []) {
text += collectText(child);
}
return text;
}
/** Find all elements matching a class attribute value anywhere in the tree. */
function findByClass(node: any, cls: string): any[] {
const results: any[] = [];
if (!node || typeof node !== 'object') return results;
if (typeof node.class === 'string' && node.class.includes(cls)) {
results.push(node);
}
for (const child of node.children || []) {
results.push(...findByClass(child, cls));
}
return results;
}
/** Extract text from the second argument of the last showModal call. */
function getModalContent(): any {
const calls = mocks.showModal.mock.calls;
return calls[calls.length - 1]?.[1];
}
// Default "happy path" leak result
function makeSuccessResult(overrides: Record<string, any> = {}) {
return {
success: true as const,
data: {
ip_leak: {
leaked: false,
direct_ip: '95.173.136.25',
direct_country: 'Russia',
direct_city: 'Moscow',
direct_isp: 'Rostelecom',
proxy_ip: '185.220.101.5',
proxy_country: 'Netherlands',
proxy_city: 'Amsterdam',
proxy_org: 'Mullvad',
proxy_online: true,
},
dns_leak: {
dns_leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '185.220.101.5',
dns_servers: [
{
ip: '1.1.1.1',
country: 'United States',
isp: 'Cloudflare',
is_isp: false,
},
],
direct_dns_servers: [
{
ip: '212.188.4.10',
country: 'Russia',
isp: 'Rostelecom',
is_isp: true,
},
],
proxy_online: true,
},
...overrides,
},
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('renderLeakCheckModal', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.showModal.mockReset();
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
success: true,
data: {
ip_leak: {
leaked: false,
direct_ip: '95.173.136.25',
direct_country: 'Russia',
direct_isp: 'Rostelecom',
proxy_ip: '185.220.101.5',
proxy_country: 'Netherlands',
proxy_org: 'Mullvad',
proxy_online: true,
},
dns_leak: {
dns_leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '185.220.101.5',
dns_servers: [
{
ip: '1.1.1.1',
country: 'United States',
isp: 'Cloudflare',
is_isp: false,
},
],
direct_dns_servers: [
{
ip: '212.188.4.10',
country: 'Russia',
isp: 'Rostelecom',
is_isp: true,
},
],
proxy_online: true,
},
},
});
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult(),
);
});
it('renders and displays the IP & DNS Leak Detection modal', () => {
renderLeakCheckModal();
// ── Modal lifecycle ────────────────────────────────────────────────────────
it('renders the modal with correct title', () => {
renderLeakCheckModal();
expect(mocks.showModal).toHaveBeenCalledTimes(1);
expect(mocks.showModal).toHaveBeenCalledWith(
expect.stringContaining('Tachyon IP & DNS Leak Detection'),
@@ -155,49 +202,118 @@ describe('renderLeakCheckModal', () => {
);
});
it('calls TachyonShellMethods.leakCheck on start', async () => {
const leakCheckSpy = vi.spyOn(TachyonShellMethods, 'leakCheck');
it('auto-starts the leak check on open', () => {
const spy = vi.spyOn(TachyonShellMethods, 'leakCheck');
renderLeakCheckModal();
expect(leakCheckSpy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(1);
});
it('handles leak detection when IP is leaked', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
success: true,
data: {
// ── IP section — SECURE path ───────────────────────────────────────────────
it('shows SECURE alert when proxy is online and IPs differ', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('SECURE');
expect(text).toContain('185.220.101.5');
});
});
it('shows SECURE badge (not DIRECT or INACTIVE) when IPs differ', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const badges = findByClass(content, 'badge');
const badgeTexts = badges.map((b) => collectText(b)).join(' ');
expect(badgeTexts).toContain('SECURE');
expect(badgeTexts).not.toContain('DIRECT');
expect(badgeTexts).not.toContain('INACTIVE');
});
});
it('alert class is success when proxy is online and IPs differ', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
// Find the IP alert div
const alertDivs = findByClass(content, 'alert-message');
const successAlerts = alertDivs.filter((d) =>
d.class?.includes('success'),
);
expect(successAlerts.length).toBeGreaterThan(0);
});
});
// ── IP section — DIRECT / leaked path ─────────────────────────────────────
it('shows DIRECT badge when proxy_ip equals direct_ip', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
ip_leak: {
leaked: true,
direct_ip: '95.173.136.25',
proxy_ip: '95.173.136.25',
proxy_online: true,
},
dns_leak: {
dns_leaked: true,
direct_ip: '95.173.136.25',
proxy_ip: '95.173.136.25',
dns_servers: [
{
ip: '212.188.4.10',
country: 'Russia',
isp: 'Rostelecom',
is_isp: true,
},
],
direct_dns_servers: [],
proxy_online: true,
},
},
});
}),
);
renderLeakCheckModal();
expect(mocks.showModal).toHaveBeenCalledTimes(1);
await vi.waitFor(() => {
const content = getModalContent();
const badges = findByClass(content, 'badge');
const badgeTexts = badges.map((b) => collectText(b)).join(' ');
expect(badgeTexts).toContain('DIRECT');
});
});
it('handles offline proxy gracefully', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
success: true,
data: {
it('shows warning alert class when IP is leaked', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
ip_leak: {
leaked: true,
direct_ip: '95.173.136.25',
proxy_ip: '95.173.136.25',
proxy_online: true,
},
}),
);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const alertDivs = findByClass(content, 'alert-message');
const warningAlerts = alertDivs.filter((d) =>
d.class?.includes('warning'),
);
expect(warningAlerts.length).toBeGreaterThan(0);
});
});
// ── IP section — proxy offline ─────────────────────────────────────────────
it('shows INACTIVE badge when proxy_online=false', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
ip_leak: {
leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '—',
proxy_online: false,
},
}),
);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const badges = findByClass(content, 'badge');
const badgeTexts = badges.map((b) => collectText(b)).join(' ');
expect(badgeTexts).toContain('INACTIVE');
});
});
it('shows info alert class when proxy offline', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
ip_leak: {
leaked: false,
direct_ip: '95.173.136.25',
@@ -212,10 +328,151 @@ describe('renderLeakCheckModal', () => {
direct_dns_servers: [],
proxy_online: false,
},
},
});
}),
);
renderLeakCheckModal();
expect(mocks.showModal).toHaveBeenCalledTimes(1);
await vi.waitFor(() => {
const content = getModalContent();
const alertDivs = findByClass(content, 'alert-message');
const infoAlerts = alertDivs.filter((d) => d.class?.includes('info'));
expect(infoAlerts.length).toBeGreaterThan(0);
});
});
// ── DNS section ────────────────────────────────────────────────────────────
it('renders proxy DNS resolvers with "via Proxy" path label', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('1.1.1.1');
expect(text).toContain('via Proxy');
});
});
it('renders direct_dns_servers with "via WAN" path label', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
// direct_dns_servers IP from default mock
expect(text).toContain('212.188.4.10');
expect(text).toContain('via WAN');
});
});
it('marks ISP DNS servers with ISP DNS badge', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const badges = findByClass(content, 'badge');
const badgeTexts = badges.map((b) => collectText(b)).join(' ');
expect(badgeTexts).toContain('ISP DNS');
});
});
it('marks non-ISP DNS servers with SAFE badge', async () => {
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const badges = findByClass(content, 'badge');
const badgeTexts = badges.map((b) => collectText(b)).join(' ');
expect(badgeTexts).toContain('SAFE');
});
});
it('shows direct DNS servers even when proxy offline', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
ip_leak: {
leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '—',
proxy_online: false,
},
dns_leak: {
dns_leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '—',
dns_servers: [],
direct_dns_servers: [
{ ip: '8.8.8.8', country: 'US', isp: 'Google', is_isp: false },
],
proxy_online: false,
},
}),
);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('8.8.8.8');
expect(text).toContain('via WAN');
});
});
it('shows "No DNS resolvers recorded" when both lists are empty', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
makeSuccessResult({
dns_leak: {
dns_leaked: false,
direct_ip: '95.173.136.25',
proxy_ip: '185.220.101.5',
dns_servers: [],
direct_dns_servers: [],
proxy_online: true,
},
}),
);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('No DNS resolvers recorded');
});
});
// ── Error handling ─────────────────────────────────────────────────────────
it('shows error alert when leakCheck returns success=false', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
success: false,
error: 'Connection timeout',
} as any);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('Connection timeout');
});
});
it('shows error alert on unexpected exception from leakCheck', async () => {
vi.spyOn(TachyonShellMethods, 'leakCheck').mockRejectedValue(
new Error('Network error'),
);
renderLeakCheckModal();
await vi.waitFor(() => {
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('Network error');
});
});
// ── Re-run button ──────────────────────────────────────────────────────────
it('renders Re-run Leak Test button', () => {
renderLeakCheckModal();
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('Re-run Leak Test');
});
it('renders Close button', () => {
renderLeakCheckModal();
const content = getModalContent();
const text = collectText(content);
expect(text).toContain('Close');
});
});