135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
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' },
|
|
],
|
|
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 });
|
|
}
|