fix(tracer): resolve discord community cidr filter (#50) and improve leak check display
This commit is contained in:
@@ -128,7 +128,7 @@ export function renderLeakCheckModal() {
|
|||||||
const ipAlertClass = !ip_leak.proxy_online
|
const ipAlertClass = !ip_leak.proxy_online
|
||||||
? 'alert-message info'
|
? 'alert-message info'
|
||||||
: isDirectRouting
|
: isDirectRouting
|
||||||
? 'alert-message info'
|
? 'alert-message warning'
|
||||||
: 'alert-message success';
|
: 'alert-message success';
|
||||||
|
|
||||||
const ipAlertText = !ip_leak.proxy_online
|
const ipAlertText = !ip_leak.proxy_online
|
||||||
@@ -266,32 +266,36 @@ export function renderLeakCheckModal() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- 2. DNS Leak Section ---
|
// --- 2. DNS Leak Section ---
|
||||||
const dnsAlertClass =
|
const proxyDnsServers = dns_leak.dns_servers || [];
|
||||||
!dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0
|
const directDnsServers = dns_leak.direct_dns_servers || [];
|
||||||
? 'alert-message info'
|
const hasProxyDns = proxyDnsServers.length > 0;
|
||||||
: dns_leak.dns_leaked
|
|
||||||
? 'alert-message warning'
|
|
||||||
: 'alert-message success';
|
|
||||||
|
|
||||||
const dnsAlertText =
|
const dnsAlertClass = !hasProxyDns
|
||||||
!dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0
|
? '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
|
: _(
|
||||||
? _(
|
'🛡️ SECURE: All DNS queries are resolved through independent secure DNS servers.',
|
||||||
'ℹ️ 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.',
|
|
||||||
);
|
|
||||||
|
|
||||||
const dnsTableRows = (dns_leak.dns_servers || []).map((s) =>
|
const makeDnsRow = (s: Tachyon.DNSResolverInfo, pathLabel: string) =>
|
||||||
E('tr', { class: 'tr cbi-section-table-row' }, [
|
E('tr', { class: 'tr cbi-section-table-row' }, [
|
||||||
E('td', { class: 'td' }, [E('code', {}, s.ip)]),
|
E('td', { class: 'td' }, [E('code', {}, s.ip)]),
|
||||||
E('td', { class: 'td' }, s.country || '—'),
|
E('td', { class: 'td' }, s.country || '—'),
|
||||||
E('td', { class: 'td' }, s.isp || '—'),
|
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
|
s.is_isp
|
||||||
? E(
|
? E(
|
||||||
'span',
|
'span',
|
||||||
@@ -311,9 +315,24 @@ export function renderLeakCheckModal() {
|
|||||||
},
|
},
|
||||||
_('SAFE'),
|
_('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(
|
const dnsTable = E(
|
||||||
'table',
|
'table',
|
||||||
@@ -332,20 +351,21 @@ export function renderLeakCheckModal() {
|
|||||||
{ class: 'th', style: 'text-align: center;' },
|
{ class: 'th', style: 'text-align: center;' },
|
||||||
_('Verdict'),
|
_('Verdict'),
|
||||||
),
|
),
|
||||||
|
E('th', { class: 'th' }, _('Path')),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
E(
|
E(
|
||||||
'tbody',
|
'tbody',
|
||||||
{},
|
{},
|
||||||
dnsTableRows.length > 0
|
allDnsRows.length > 0
|
||||||
? dnsTableRows
|
? allDnsRows
|
||||||
: [
|
: [
|
||||||
E('tr', { class: 'tr' }, [
|
E('tr', { class: 'tr' }, [
|
||||||
E(
|
E(
|
||||||
'td',
|
'td',
|
||||||
{
|
{
|
||||||
class: 'td',
|
class: 'td',
|
||||||
colSpan: 4,
|
colSpan: 5,
|
||||||
style: 'text-align: center; opacity: 0.7;',
|
style: 'text-align: center; opacity: 0.7;',
|
||||||
},
|
},
|
||||||
_('No DNS resolvers recorded'),
|
_('No DNS resolvers recorded'),
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Hoisted mocks — must come before any imports that transitively use globals
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
const mocks = vi.hoisted(() => {
|
const mocks = vi.hoisted(() => {
|
||||||
class MockMutationObserver {
|
// Minimal DOM mock that tracks tree structure for inspection
|
||||||
observe() {}
|
function createEl(tag: string) {
|
||||||
disconnect() {}
|
const el: any = {
|
||||||
takeRecords() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(globalThis as any).MutationObserver = MockMutationObserver;
|
|
||||||
|
|
||||||
function createDummyElement(tag: string) {
|
|
||||||
return {
|
|
||||||
tagName: tag.toUpperCase(),
|
tagName: tag.toUpperCase(),
|
||||||
children: [] as any[],
|
children: [] as any[],
|
||||||
style: {} as Record<string, string>,
|
style: {} as Record<string, string>,
|
||||||
textContent: '',
|
textContent: '',
|
||||||
innerHTML: '',
|
innerHTML: '',
|
||||||
|
class: '',
|
||||||
|
colSpan: 0,
|
||||||
setAttribute() {},
|
setAttribute() {},
|
||||||
getAttribute() {
|
getAttribute() {
|
||||||
return null;
|
return null;
|
||||||
@@ -25,27 +22,28 @@ const mocks = vi.hoisted(() => {
|
|||||||
addEventListener() {},
|
addEventListener() {},
|
||||||
querySelectorAll: () => [],
|
querySelectorAll: () => [],
|
||||||
querySelector: () => null,
|
querySelector: () => null,
|
||||||
replaceChildren: function (...nodes: any[]) {
|
replaceChildren(...nodes: any[]) {
|
||||||
(this as any).children = nodes;
|
(this as any).children = nodes;
|
||||||
},
|
},
|
||||||
appendChild: function (node: any) {
|
appendChild(node: any) {
|
||||||
(this as any).children.push(node);
|
if (node && typeof node === 'object') {
|
||||||
|
(this as any).children.push(node);
|
||||||
|
} else if (node) {
|
||||||
|
(this as any).textContent += String(node);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
append: function (...nodes: any[]) {
|
append(...nodes: any[]) {
|
||||||
nodes.forEach((node) => (this as any).children.push(node));
|
nodes.forEach((n) => (this as any).appendChild(n));
|
||||||
},
|
|
||||||
classList: {
|
|
||||||
add() {},
|
|
||||||
remove() {},
|
|
||||||
},
|
},
|
||||||
|
classList: { add() {}, remove() {} },
|
||||||
};
|
};
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
(globalThis as any).document = {
|
(globalThis as any).document = {
|
||||||
body: {} as any,
|
body: {} as any,
|
||||||
createElement: (tag: string) => createDummyElement(tag) as any,
|
createElement: (tag: string) => createEl(tag),
|
||||||
createElementNS: (_ns: string, tag: string) =>
|
createElementNS: (_ns: string, tag: string) => createEl(tag),
|
||||||
createDummyElement(tag) as any,
|
|
||||||
getElementById: () => null,
|
getElementById: () => null,
|
||||||
querySelector: () => null,
|
querySelector: () => null,
|
||||||
querySelectorAll: () => [],
|
querySelectorAll: () => [],
|
||||||
@@ -59,18 +57,16 @@ const mocks = vi.hoisted(() => {
|
|||||||
if (style && typeof style === 'string') {
|
if (style && typeof style === 'string') {
|
||||||
style.split(';').forEach((rule: string) => {
|
style.split(';').forEach((rule: string) => {
|
||||||
const [k, v] = rule.split(':').map((s: string) => s.trim());
|
const [k, v] = rule.split(':').map((s: string) => s.trim());
|
||||||
if (k && v) {
|
if (k && v) el.style[k] = v;
|
||||||
el.style[k] = v;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else if (style && typeof style === 'object') {
|
} else if (style && typeof style === 'object') {
|
||||||
Object.assign(el.style, style);
|
Object.assign(el.style, style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (children) {
|
if (children !== undefined && children !== null) {
|
||||||
if (Array.isArray(children)) {
|
if (Array.isArray(children)) {
|
||||||
children.forEach((c) => {
|
children.forEach((c) => {
|
||||||
if (c) el.appendChild(c);
|
if (c != null) el.appendChild(c);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
el.appendChild(children);
|
el.appendChild(children);
|
||||||
@@ -80,74 +76,125 @@ const mocks = vi.hoisted(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(globalThis as any)._ = (str: string) => str;
|
(globalThis as any)._ = (str: string) => str;
|
||||||
|
|
||||||
(globalThis as any).rpc = { declare: vi.fn() };
|
(globalThis as any).rpc = { declare: vi.fn() };
|
||||||
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
|
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
|
||||||
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
|
(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 showModal = vi.fn();
|
||||||
const hideModal = vi.fn();
|
const hideModal = vi.fn();
|
||||||
(globalThis as any).ui = {
|
(globalThis as any).ui = { showModal, hideModal, addNotification: vi.fn() };
|
||||||
showModal,
|
|
||||||
hideModal,
|
|
||||||
addNotification: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return { showModal, hideModal };
|
||||||
showModal,
|
|
||||||
hideModal,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Imports after mocks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
import { renderLeakCheckModal } from '../partials/renderLeakCheckModal';
|
import { renderLeakCheckModal } from '../partials/renderLeakCheckModal';
|
||||||
import { TachyonShellMethods } from '../../../methods/shell';
|
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', () => {
|
describe('renderLeakCheckModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.showModal.mockReset();
|
mocks.showModal.mockReset();
|
||||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
|
||||||
success: true,
|
makeSuccessResult(),
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders and displays the IP & DNS Leak Detection modal', () => {
|
// ── Modal lifecycle ────────────────────────────────────────────────────────
|
||||||
renderLeakCheckModal();
|
|
||||||
|
|
||||||
|
it('renders the modal with correct title', () => {
|
||||||
|
renderLeakCheckModal();
|
||||||
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
expect(mocks.showModal).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.showModal).toHaveBeenCalledWith(
|
expect(mocks.showModal).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Tachyon IP & DNS Leak Detection'),
|
expect.stringContaining('Tachyon IP & DNS Leak Detection'),
|
||||||
@@ -155,49 +202,118 @@ describe('renderLeakCheckModal', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls TachyonShellMethods.leakCheck on start', async () => {
|
it('auto-starts the leak check on open', () => {
|
||||||
const leakCheckSpy = vi.spyOn(TachyonShellMethods, 'leakCheck');
|
const spy = vi.spyOn(TachyonShellMethods, 'leakCheck');
|
||||||
renderLeakCheckModal();
|
renderLeakCheckModal();
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
expect(leakCheckSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles leak detection when IP is leaked', async () => {
|
// ── IP section — SECURE path ───────────────────────────────────────────────
|
||||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
|
||||||
success: true,
|
it('shows SECURE alert when proxy is online and IPs differ', async () => {
|
||||||
data: {
|
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: {
|
ip_leak: {
|
||||||
leaked: true,
|
leaked: true,
|
||||||
direct_ip: '95.173.136.25',
|
direct_ip: '95.173.136.25',
|
||||||
proxy_ip: '95.173.136.25',
|
proxy_ip: '95.173.136.25',
|
||||||
proxy_online: true,
|
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();
|
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 () => {
|
it('shows warning alert class when IP is leaked', async () => {
|
||||||
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue({
|
vi.spyOn(TachyonShellMethods, 'leakCheck').mockResolvedValue(
|
||||||
success: true,
|
makeSuccessResult({
|
||||||
data: {
|
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: {
|
ip_leak: {
|
||||||
leaked: false,
|
leaked: false,
|
||||||
direct_ip: '95.173.136.25',
|
direct_ip: '95.173.136.25',
|
||||||
@@ -212,10 +328,151 @@ describe('renderLeakCheckModal', () => {
|
|||||||
direct_dns_servers: [],
|
direct_dns_servers: [],
|
||||||
proxy_online: false,
|
proxy_online: false,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
});
|
);
|
||||||
|
|
||||||
renderLeakCheckModal();
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15082,7 +15082,7 @@ function renderLeakCheckModal() {
|
|||||||
resultsContainer.innerHTML = "";
|
resultsContainer.innerHTML = "";
|
||||||
const { ip_leak, dns_leak } = data;
|
const { ip_leak, dns_leak } = data;
|
||||||
const isDirectRouting = ip_leak.proxy_online && (ip_leak.leaked || ip_leak.direct_ip === ip_leak.proxy_ip);
|
const isDirectRouting = ip_leak.proxy_online && (ip_leak.leaked || ip_leak.direct_ip === ip_leak.proxy_ip);
|
||||||
const ipAlertClass = !ip_leak.proxy_online ? "alert-message info" : isDirectRouting ? "alert-message info" : "alert-message success";
|
const ipAlertClass = !ip_leak.proxy_online ? "alert-message info" : isDirectRouting ? "alert-message warning" : "alert-message success";
|
||||||
const ipAlertText = !ip_leak.proxy_online ? _("Proxy outbound is inactive or not configured for local testing.") : isDirectRouting ? _(
|
const ipAlertText = !ip_leak.proxy_online ? _("Proxy outbound is inactive or not configured for local testing.") : isDirectRouting ? _(
|
||||||
"ℹ️ Direct connection (WAN): Public IP matches your ISP. Under selective routing (by domains or blocklists), unblocked resources bypass the proxy — this is standard operation."
|
"ℹ️ Direct connection (WAN): Public IP matches your ISP. Under selective routing (by domains or blocklists), unblocked resources bypass the proxy — this is standard operation."
|
||||||
) : _("🛡️ SECURE: Public IP is concealed behind the proxy outbound.");
|
) : _("🛡️ SECURE: Public IP is concealed behind the proxy outbound.");
|
||||||
@@ -15195,38 +15195,56 @@ function renderLeakCheckModal() {
|
|||||||
ipTable
|
ipTable
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
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 dnsAlertText = !dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0 ? _(
|
const directDnsServers = dns_leak.direct_dns_servers || [];
|
||||||
"No DNS resolvers captured through proxy (proxy is offline or test domain is not intercepted)."
|
const hasProxyDns = proxyDnsServers.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 ? _(
|
) : 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."
|
"ℹ️ 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(
|
const makeDnsRow = (s, pathLabel) => E("tr", { class: "tr cbi-section-table-row" }, [
|
||||||
(s) => E("tr", { class: "tr cbi-section-table-row" }, [
|
E("td", { class: "td" }, [E("code", {}, s.ip)]),
|
||||||
E("td", { class: "td" }, [E("code", {}, s.ip)]),
|
E("td", { class: "td" }, s.country || "—"),
|
||||||
E("td", { class: "td" }, s.country || "—"),
|
E("td", { class: "td" }, s.isp || "—"),
|
||||||
E("td", { class: "td" }, s.isp || "—"),
|
E(
|
||||||
E("td", { class: "td", style: "text-align: center;" }, [
|
"td",
|
||||||
s.is_isp ? E(
|
{ class: "td", style: "text-align: center;" },
|
||||||
"span",
|
s.is_isp ? E(
|
||||||
{
|
"span",
|
||||||
class: "badge",
|
{
|
||||||
style: "background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
class: "badge",
|
||||||
},
|
style: "background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||||
_("ISP DNS")
|
},
|
||||||
) : E(
|
_("ISP DNS")
|
||||||
"span",
|
) : E(
|
||||||
{
|
"span",
|
||||||
class: "badge",
|
{
|
||||||
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
class: "badge",
|
||||||
},
|
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||||
_("SAFE")
|
},
|
||||||
)
|
_("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(
|
const dnsTable = E(
|
||||||
"table",
|
"table",
|
||||||
{
|
{
|
||||||
@@ -15243,19 +15261,20 @@ function renderLeakCheckModal() {
|
|||||||
"th",
|
"th",
|
||||||
{ class: "th", style: "text-align: center;" },
|
{ class: "th", style: "text-align: center;" },
|
||||||
_("Verdict")
|
_("Verdict")
|
||||||
)
|
),
|
||||||
|
E("th", { class: "th" }, _("Path"))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
E(
|
E(
|
||||||
"tbody",
|
"tbody",
|
||||||
{},
|
{},
|
||||||
dnsTableRows.length > 0 ? dnsTableRows : [
|
allDnsRows.length > 0 ? allDnsRows : [
|
||||||
E("tr", { class: "tr" }, [
|
E("tr", { class: "tr" }, [
|
||||||
E(
|
E(
|
||||||
"td",
|
"td",
|
||||||
{
|
{
|
||||||
class: "td",
|
class: "td",
|
||||||
colSpan: 4,
|
colSpan: 5,
|
||||||
style: "text-align: center; opacity: 0.7;"
|
style: "text-align: center; opacity: 0.7;"
|
||||||
},
|
},
|
||||||
_("No DNS resolvers recorded")
|
_("No DNS resolvers recorded")
|
||||||
|
|||||||
@@ -12130,9 +12130,6 @@ async function performTrace(query) {
|
|||||||
|
|
||||||
for (const community of communityLists) {
|
for (const community of communityLists) {
|
||||||
let subnets = await readSubnetListFile(community);
|
let subnets = await readSubnetListFile(community);
|
||||||
if (community === "discord") {
|
|
||||||
subnets = subnets.filter((s) => !isCloudflareSharedCidr(s));
|
|
||||||
}
|
|
||||||
const matchedCommunityCidr = matchIpInCidrs(ip, subnets);
|
const matchedCommunityCidr = matchIpInCidrs(ip, subnets);
|
||||||
if (matchedCommunityCidr) {
|
if (matchedCommunityCidr) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ CONFIG="$WORK_DIR/singbox.json"
|
|||||||
node -e '
|
node -e '
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const cfg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
const cfg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||||
const rules = cfg.route?.rules || [];
|
const rules = (cfg.route && cfg.route.rules) || [];
|
||||||
|
|
||||||
const hijackDnsPort = rules.find(r => r.action === "hijack-dns" && r.inbound === "server-home_node-in" && r.port === 53);
|
const hijackDnsPort = rules.find(r => r.action === "hijack-dns" && r.inbound === "server-home_node-in" && r.port === 53);
|
||||||
if (!hijackDnsPort) {
|
if (!hijackDnsPort) {
|
||||||
|
|||||||
Reference in New Issue
Block a user