import fs from 'node:fs'; import { settings } from './config.js'; import { HarborError } from '../shared/errors.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'; function findOutbound(subscriptionConfig, selectedTag) { const outbounds = Array.isArray(subscriptionConfig?.outbounds) ? subscriptionConfig.outbounds : []; const tag = String(selectedTag || '').trim(); return outbounds.find((outbound) => ( String(outbound.tag || '').trim() === tag && PROXY_TYPES.has(outbound.type) )); } export function buildGatewayConfig(subscriptionConfig, selectedTag, { clientDirect = false } = {}) { const clientMode = settings.appMode === 'client'; const directClient = clientMode && clientDirect; const vpnOutbound = directClient ? null : structuredClone(findOutbound(subscriptionConfig, selectedTag)); if (!directClient && !vpnOutbound) throw new HarborError('SERVER_NOT_FOUND'); if (vpnOutbound && !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, sniff: true, sniff_override_destination: true, }] : []), { type: 'mixed', tag: MIXED_INBOUND, listen: settings.bindIp, listen_port: settings.proxyPort, sniff: true, set_system_proxy: false, }, ]; const rules = clientMode ? [{ inbound: [MIXED_INBOUND], outbound: outboundTag }] : [ { 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 }, }, dns: { independent_cache: true }, inbounds, outbounds: [ ...(vpnOutbound ? [vpnOutbound] : []), { type: 'direct', tag: 'direct' }, { type: 'block', tag: 'block' }, ], route: { rule_set: [], rules, final: outboundTag, ...(clientMode ? {} : { auto_detect_interface: true }), }, }; } export function writeSingboxConfig(config) { atomicWriteJson(settings.configPath, config); } export function restoreSingboxConfig(contents) { atomicWriteFile(settings.configPath, contents); } export function removeSingboxConfig() { fs.rmSync(settings.configPath, { force: true }); }