fix(singbox): prioritize proxy outbounds for ip.podkop.fyi check and failover regardless of DPI section order
This commit is contained in:
@@ -54,10 +54,13 @@ export function renderCircleAlertIcon24() {
|
||||
}
|
||||
|
||||
export function renderCircleCheckBigIcon24() {
|
||||
return createIcon('lucide lucide-circle-check-big-icon lucide-circle-check-big', [
|
||||
svgEl('path', { d: 'M21.801 10A10 10 0 1 1 17 3.335' }),
|
||||
svgEl('path', { d: 'm9 11 3 3L22 4' }),
|
||||
]);
|
||||
return createIcon(
|
||||
'lucide lucide-circle-check-big-icon lucide-circle-check-big',
|
||||
[
|
||||
svgEl('path', { d: 'M21.801 10A10 10 0 1 1 17 3.335' }),
|
||||
svgEl('path', { d: 'm9 11 3 3L22 4' }),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export function renderCircleCheckIcon24() {
|
||||
@@ -258,12 +261,15 @@ export function renderSendIcon24() {
|
||||
}
|
||||
|
||||
export function renderSquareChartGanttIcon24() {
|
||||
return createIcon('lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt', [
|
||||
svgEl('rect', { width: '18', height: '18', x: '3', y: '3', rx: '2' }),
|
||||
svgEl('path', { d: 'M9 8h7' }),
|
||||
svgEl('path', { d: 'M8 12h6' }),
|
||||
svgEl('path', { d: 'M11 16h5' }),
|
||||
]);
|
||||
return createIcon(
|
||||
'lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt',
|
||||
[
|
||||
svgEl('rect', { width: '18', height: '18', x: '3', y: '3', rx: '2' }),
|
||||
svgEl('path', { d: 'M9 8h7' }),
|
||||
svgEl('path', { d: 'M8 12h6' }),
|
||||
svgEl('path', { d: 'M11 16h5' }),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export function renderTriangleAlertIcon24() {
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { insertIf } from '../../../../helpers';
|
||||
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
|
||||
import { TachyonShellMethods, RemoteFakeIPMethods } from '../../../methods';
|
||||
import type { IDiagnosticsChecksItem } from '../../../services';
|
||||
import { getDashboardSections } from '../../../methods/custom/getDashboardSections';
|
||||
import { store, type IDiagnosticsChecksItem } from '../../../services';
|
||||
import { updateCheckStore } from './updateCheckStore';
|
||||
import { getMeta } from '../helpers/getMeta';
|
||||
|
||||
const PROXY_ACTIONS = new Set([
|
||||
'connection',
|
||||
'proxy',
|
||||
'outbound',
|
||||
'vpn',
|
||||
'awg',
|
||||
'warp',
|
||||
'anytls',
|
||||
'snell',
|
||||
'mieru',
|
||||
'sudoku',
|
||||
'masque',
|
||||
'openvpn',
|
||||
]);
|
||||
|
||||
export async function runFakeIPCheck() {
|
||||
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP;
|
||||
|
||||
@@ -25,6 +41,23 @@ export async function runFakeIPCheck() {
|
||||
? ''
|
||||
: checkFakeIPResponse.message;
|
||||
|
||||
let hasProxySection = true;
|
||||
try {
|
||||
const cachedSections = store.get().sectionsWidget?.data;
|
||||
const sections =
|
||||
cachedSections && cachedSections.length > 0
|
||||
? cachedSections
|
||||
: (await getDashboardSections({ includeSubscriptionCopyState: false }))
|
||||
.data || [];
|
||||
if (sections.length > 0) {
|
||||
hasProxySection = sections.some(
|
||||
(s) => s.action && PROXY_ACTIONS.has(s.action),
|
||||
);
|
||||
}
|
||||
} catch (_e) {
|
||||
hasProxySection = true;
|
||||
}
|
||||
|
||||
const checks = {
|
||||
singBoxFakeIP:
|
||||
routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip,
|
||||
@@ -38,8 +71,9 @@ export async function runFakeIPCheck() {
|
||||
};
|
||||
|
||||
const fakeIPWorks = checks.singBoxFakeIP && checks.browserFakeIP;
|
||||
const isDirectOnly = fakeIPWorks && !hasProxySection && !checks.differentIP;
|
||||
const { state, description } = fakeIPWorks
|
||||
? checks.differentIP
|
||||
? checks.differentIP || isDirectOnly
|
||||
? { state: 'success' as const, description: _('Checks passed') }
|
||||
: {
|
||||
state: 'warning' as const,
|
||||
@@ -84,13 +118,13 @@ export async function runFakeIPCheck() {
|
||||
},
|
||||
...insertIf<IDiagnosticsChecksItem>(checks.browserFakeIP, [
|
||||
{
|
||||
state: checks.differentIP ? 'success' : 'warning',
|
||||
state: checks.differentIP || isDirectOnly ? 'success' : 'warning',
|
||||
key: !checks.canComparePublicIP
|
||||
? _('Could not compare FakeIP and control public IPs')
|
||||
: checks.differentIP
|
||||
? _('FakeIP and control checks use different public IPs')
|
||||
: _('FakeIP and control checks use the same public IP'),
|
||||
value: '',
|
||||
value: isDirectOnly ? _('Direct connection via ISP') : '',
|
||||
},
|
||||
]),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
checkFakeIP: vi.fn(),
|
||||
getFakeIpCheck: vi.fn(),
|
||||
getIpCheck: vi.fn(),
|
||||
getDashboardSections: vi.fn(),
|
||||
updateCheckStore: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../../methods', () => ({
|
||||
TachyonShellMethods: {
|
||||
checkFakeIP: mocks.checkFakeIP,
|
||||
},
|
||||
RemoteFakeIPMethods: {
|
||||
getFakeIpCheck: mocks.getFakeIpCheck,
|
||||
getIpCheck: mocks.getIpCheck,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../../methods/custom/getDashboardSections', () => ({
|
||||
getDashboardSections: mocks.getDashboardSections,
|
||||
}));
|
||||
|
||||
vi.mock('../updateCheckStore', () => ({
|
||||
updateCheckStore: mocks.updateCheckStore,
|
||||
}));
|
||||
|
||||
vi.mock('../../../../services', () => ({
|
||||
store: {
|
||||
get: () => ({
|
||||
sectionsWidget: {
|
||||
data: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
import { runFakeIPCheck } from '../runFakeIPCheck';
|
||||
|
||||
describe('runFakeIPCheck', () => {
|
||||
beforeEach(() => {
|
||||
mocks.checkFakeIP.mockReset();
|
||||
mocks.getFakeIpCheck.mockReset();
|
||||
mocks.getIpCheck.mockReset();
|
||||
mocks.getDashboardSections.mockReset();
|
||||
mocks.updateCheckStore.mockReset();
|
||||
});
|
||||
|
||||
it('marks check as success when proxy is present and public IPs differ', async () => {
|
||||
mocks.checkFakeIP.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '198.18.1.1' },
|
||||
});
|
||||
mocks.getFakeIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '1.2.3.4' },
|
||||
});
|
||||
mocks.getIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: false, IP: '5.6.7.8' },
|
||||
});
|
||||
mocks.getDashboardSections.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
{ sectionName: 'Discord', action: 'zapret2' },
|
||||
{ sectionName: 'Main', action: 'connection' },
|
||||
],
|
||||
});
|
||||
|
||||
await runFakeIPCheck();
|
||||
|
||||
expect(mocks.updateCheckStore).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
state: 'success',
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
state: 'success',
|
||||
key: 'FakeIP and control checks use different public IPs',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('marks check as warning when proxy is present but public IPs are identical', async () => {
|
||||
mocks.checkFakeIP.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '198.18.1.1' },
|
||||
});
|
||||
mocks.getFakeIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '1.2.3.4' },
|
||||
});
|
||||
mocks.getIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: false, IP: '1.2.3.4' },
|
||||
});
|
||||
mocks.getDashboardSections.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ sectionName: 'Main', action: 'connection' }],
|
||||
});
|
||||
|
||||
await runFakeIPCheck();
|
||||
|
||||
expect(mocks.updateCheckStore).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
state: 'warning',
|
||||
description: 'FakeIP works; public IP comparison is inconclusive',
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
state: 'warning',
|
||||
key: 'FakeIP and control checks use the same public IP',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('marks check as success when only DPI bypass sections exist and public IPs match', async () => {
|
||||
mocks.checkFakeIP.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '198.18.1.1' },
|
||||
});
|
||||
mocks.getFakeIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: true, IP: '1.2.3.4' },
|
||||
});
|
||||
mocks.getIpCheck.mockResolvedValue({
|
||||
success: true,
|
||||
data: { fakeip: false, IP: '1.2.3.4' },
|
||||
});
|
||||
mocks.getDashboardSections.mockResolvedValue({
|
||||
success: true,
|
||||
data: [
|
||||
{ sectionName: 'Discord', action: 'zapret2' },
|
||||
{ sectionName: 'YouTube', action: 'zapret2' },
|
||||
],
|
||||
});
|
||||
|
||||
await runFakeIPCheck();
|
||||
|
||||
expect(mocks.updateCheckStore).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
state: 'success',
|
||||
description: 'Checks passed',
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
state: 'success',
|
||||
key: 'FakeIP and control checks use the same public IP',
|
||||
value: 'Direct connection via ISP',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,13 @@
|
||||
// tests/setup/global-mocks.ts
|
||||
globalThis._ = (key: string) => key;
|
||||
(globalThis as any).rpc = { declare: () => () => Promise.resolve({}) };
|
||||
if (typeof (globalThis as any).localStorage === 'undefined') {
|
||||
const storage = new Map<string, string>();
|
||||
(globalThis as any).localStorage = {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, val: string) => storage.set(key, `${val}`),
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
clear: () => storage.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user