Refactor VPN proxy client implementation
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import fs from 'node:fs';
|
||||
import { settings } from './config.js';
|
||||
import { HarborError } from '../shared/errors.js';
|
||||
import { normalizeRouteRules } from '../shared/routingRules.js';
|
||||
import { atomicWriteFile, atomicWriteJson } from './services/stateStore.js';
|
||||
|
||||
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
|
||||
const MIXED_INBOUND = 'mixed-in';
|
||||
const TPROXY_INBOUND = 'tproxy-in';
|
||||
const DIAGNOSTICS_INBOUND = 'diagnostics-vpn-in';
|
||||
const SNIFF_TIMEOUT = '1s';
|
||||
const SNIFFERS = ['http', 'tls', 'quic'];
|
||||
|
||||
interface ProxyOutbound extends Record<string, unknown> {
|
||||
tag?: string;
|
||||
type?: string;
|
||||
packet_encoding?: string;
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function findOutbound(subscriptionConfig: unknown, selectedTag: unknown): ProxyOutbound | undefined {
|
||||
const config = record(subscriptionConfig);
|
||||
const outbounds = Array.isArray(config.outbounds)
|
||||
? config.outbounds.map(record)
|
||||
: [];
|
||||
const tag = String(selectedTag || '').trim();
|
||||
return outbounds.find((outbound) => (
|
||||
String(outbound.tag || '').trim() === tag && PROXY_TYPES.has(String(outbound.type || ''))
|
||||
));
|
||||
}
|
||||
|
||||
export function buildGatewayConfig(subscriptionConfig: unknown, selectedTag: unknown, {
|
||||
clientDirect = false,
|
||||
routeRules = [],
|
||||
}: { clientDirect?: boolean; routeRules?: unknown } = {}) {
|
||||
const clientMode = settings.appMode === 'client';
|
||||
const directClient = clientMode && clientDirect;
|
||||
const vpnOutbound = structuredClone(findOutbound(subscriptionConfig, selectedTag));
|
||||
if (!vpnOutbound) throw new HarborError('SERVER_NOT_FOUND');
|
||||
if (!vpnOutbound.tag) vpnOutbound.tag = 'vpn-out';
|
||||
if (vpnOutbound.type === 'vless' && !vpnOutbound.packet_encoding) {
|
||||
vpnOutbound.packet_encoding = 'xudp';
|
||||
}
|
||||
const outboundTag = directClient ? 'direct' : vpnOutbound.tag;
|
||||
|
||||
const inbounds = [
|
||||
...(!clientMode ? [{
|
||||
type: 'tproxy',
|
||||
tag: TPROXY_INBOUND,
|
||||
listen: '::',
|
||||
listen_port: settings.tproxyPort,
|
||||
}] : []),
|
||||
{
|
||||
type: 'mixed',
|
||||
tag: MIXED_INBOUND,
|
||||
listen: settings.bindIp,
|
||||
listen_port: settings.proxyPort,
|
||||
set_system_proxy: false,
|
||||
},
|
||||
{
|
||||
type: 'mixed',
|
||||
tag: DIAGNOSTICS_INBOUND,
|
||||
listen: '127.0.0.1',
|
||||
listen_port: settings.diagnosticsProxyPort,
|
||||
set_system_proxy: false,
|
||||
},
|
||||
];
|
||||
const directRules = normalizeRouteRules(routeRules)
|
||||
.filter((rule) => rule.enabled)
|
||||
.map((rule) => ({ [rule.type]: [rule.value], outbound: 'direct' }));
|
||||
const rules = clientMode
|
||||
? [
|
||||
{
|
||||
inbound: [MIXED_INBOUND, DIAGNOSTICS_INBOUND],
|
||||
action: 'sniff',
|
||||
sniffer: SNIFFERS,
|
||||
timeout: SNIFF_TIMEOUT,
|
||||
},
|
||||
{ inbound: [DIAGNOSTICS_INBOUND], outbound: vpnOutbound.tag },
|
||||
...directRules,
|
||||
{ inbound: [MIXED_INBOUND], outbound: outboundTag },
|
||||
]
|
||||
: [
|
||||
{
|
||||
inbound: [TPROXY_INBOUND, MIXED_INBOUND, DIAGNOSTICS_INBOUND],
|
||||
action: 'sniff',
|
||||
sniffer: SNIFFERS,
|
||||
timeout: SNIFF_TIMEOUT,
|
||||
},
|
||||
{ inbound: [DIAGNOSTICS_INBOUND], outbound: outboundTag },
|
||||
...directRules,
|
||||
{ inbound: [TPROXY_INBOUND], outbound: outboundTag },
|
||||
{ inbound: [MIXED_INBOUND], outbound: outboundTag },
|
||||
];
|
||||
|
||||
return {
|
||||
log: { level: settings.logLevel, timestamp: true },
|
||||
experimental: {
|
||||
cache_file: { enabled: true, path: settings.cachePath },
|
||||
...(!clientMode ? {
|
||||
clash_api: { external_controller: `127.0.0.1:${settings.singboxApiPort}` },
|
||||
} : {}),
|
||||
},
|
||||
dns: { independent_cache: true },
|
||||
inbounds,
|
||||
outbounds: [
|
||||
vpnOutbound,
|
||||
{ type: 'direct', tag: 'direct' },
|
||||
{ type: 'block', tag: 'block' },
|
||||
],
|
||||
route: {
|
||||
rule_set: [],
|
||||
rules,
|
||||
final: outboundTag,
|
||||
...(clientMode ? {} : { auto_detect_interface: true }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function writeSingboxConfig(config: unknown) {
|
||||
atomicWriteJson(settings.configPath, config);
|
||||
}
|
||||
|
||||
export function restoreSingboxConfig(contents: string) {
|
||||
atomicWriteFile(settings.configPath, contents);
|
||||
}
|
||||
|
||||
export function removeSingboxConfig() {
|
||||
fs.rmSync(settings.configPath, { force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user