fix(singbox): prioritize proxy outbounds for ip.podkop.fyi check and failover regardless of DPI section order

This commit is contained in:
Dushnilin
2026-09-12 20:53:47 +03:00
parent c17658eb40
commit e834971919
9 changed files with 424 additions and 39 deletions
+16 -10
View File
@@ -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(),
};
}