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
|
||||
? '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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15082,7 +15082,7 @@ function renderLeakCheckModal() {
|
||||
resultsContainer.innerHTML = "";
|
||||
const { ip_leak, dns_leak } = data;
|
||||
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 ? _(
|
||||
"ℹ️ 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.");
|
||||
@@ -15195,38 +15195,56 @@ function renderLeakCheckModal() {
|
||||
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 dnsAlertText = !dns_leak.proxy_online || (dns_leak.dns_servers || []).length === 0 ? _(
|
||||
"No DNS resolvers captured through proxy (proxy is offline or test domain is not intercepted)."
|
||||
const proxyDnsServers = dns_leak.dns_servers || [];
|
||||
const directDnsServers = dns_leak.direct_dns_servers || [];
|
||||
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 ? _(
|
||||
"ℹ️ 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) => 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;" }, [
|
||||
s.is_isp ? E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("ISP DNS")
|
||||
) : E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("SAFE")
|
||||
)
|
||||
])
|
||||
])
|
||||
const makeDnsRow = (s, pathLabel) => 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;" },
|
||||
s.is_isp ? E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #fd7e14; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("ISP DNS")
|
||||
) : E(
|
||||
"span",
|
||||
{
|
||||
class: "badge",
|
||||
style: "background: #28a745; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 11px;"
|
||||
},
|
||||
_("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",
|
||||
{
|
||||
@@ -15243,19 +15261,20 @@ function renderLeakCheckModal() {
|
||||
"th",
|
||||
{ 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")
|
||||
|
||||
@@ -12130,9 +12130,6 @@ async function performTrace(query) {
|
||||
|
||||
for (const community of communityLists) {
|
||||
let subnets = await readSubnetListFile(community);
|
||||
if (community === "discord") {
|
||||
subnets = subnets.filter((s) => !isCloudflareSharedCidr(s));
|
||||
}
|
||||
const matchedCommunityCidr = matchIpInCidrs(ip, subnets);
|
||||
if (matchedCommunityCidr) {
|
||||
return {
|
||||
|
||||
@@ -72,7 +72,7 @@ CONFIG="$WORK_DIR/singbox.json"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
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);
|
||||
if (!hijackDnsPort) {
|
||||
|
||||
Reference in New Issue
Block a user